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
@@ -35,6 +35,9 @@ public class EnrichedMarkdownManagerDelegate<T extends View, U extends BaseViewM
35
35
  case "emitLinkLayout":
36
36
  mViewManager.setEmitLinkLayout(view, value == null ? false : (boolean) value);
37
37
  break;
38
+ case "emitSelectionChange":
39
+ mViewManager.setEmitSelectionChange(view, value == null ? false : (boolean) value);
40
+ break;
38
41
  case "enableTaskListItemToggle":
39
42
  mViewManager.setEnableTaskListItemToggle(view, value == null ? true : (boolean) value);
40
43
  break;
@@ -99,4 +102,13 @@ public class EnrichedMarkdownManagerDelegate<T extends View, U extends BaseViewM
99
102
  super.setProperty(view, propName, value);
100
103
  }
101
104
  }
105
+
106
+ @Override
107
+ public void receiveCommand(T view, String commandName, ReadableArray args) {
108
+ switch (commandName) {
109
+ case "clearSelection":
110
+ mViewManager.clearSelection(view);
111
+ break;
112
+ }
113
+ }
102
114
  }
@@ -19,6 +19,7 @@ public interface EnrichedMarkdownManagerInterface<T extends View> extends ViewMa
19
19
  void setMarkdown(T view, @Nullable String value);
20
20
  void setMarkdownStyle(T view, @Nullable ReadableMap value);
21
21
  void setEmitLinkLayout(T view, boolean value);
22
+ void setEmitSelectionChange(T view, boolean value);
22
23
  void setEnableTaskListItemToggle(T view, boolean value);
23
24
  void setEnableLinkPreview(T view, boolean value);
24
25
  void setSelectable(T view, boolean value);
@@ -39,4 +40,5 @@ public interface EnrichedMarkdownManagerInterface<T extends View> extends ViewMa
39
40
  void setTextBreakStrategy(T view, @Nullable String value);
40
41
  void setLineBreakStrategyIOS(T view, @Nullable String value);
41
42
  void setWritingDirection(T view, @Nullable String value);
43
+ void clearSelection(T view);
42
44
  }
@@ -35,6 +35,9 @@ public class EnrichedMarkdownTextManagerDelegate<T extends View, U extends BaseV
35
35
  case "emitLinkLayout":
36
36
  mViewManager.setEmitLinkLayout(view, value == null ? false : (boolean) value);
37
37
  break;
38
+ case "emitSelectionChange":
39
+ mViewManager.setEmitSelectionChange(view, value == null ? false : (boolean) value);
40
+ break;
38
41
  case "enableTaskListItemToggle":
39
42
  mViewManager.setEnableTaskListItemToggle(view, value == null ? true : (boolean) value);
40
43
  break;
@@ -99,4 +102,13 @@ public class EnrichedMarkdownTextManagerDelegate<T extends View, U extends BaseV
99
102
  super.setProperty(view, propName, value);
100
103
  }
101
104
  }
105
+
106
+ @Override
107
+ public void receiveCommand(T view, String commandName, ReadableArray args) {
108
+ switch (commandName) {
109
+ case "clearSelection":
110
+ mViewManager.clearSelection(view);
111
+ break;
112
+ }
113
+ }
102
114
  }
@@ -19,6 +19,7 @@ public interface EnrichedMarkdownTextManagerInterface<T extends View> extends Vi
19
19
  void setMarkdown(T view, @Nullable String value);
20
20
  void setMarkdownStyle(T view, @Nullable ReadableMap value);
21
21
  void setEmitLinkLayout(T view, boolean value);
22
+ void setEmitSelectionChange(T view, boolean value);
22
23
  void setEnableTaskListItemToggle(T view, boolean value);
23
24
  void setEnableLinkPreview(T view, boolean value);
24
25
  void setSelectable(T view, boolean value);
@@ -39,4 +40,5 @@ public interface EnrichedMarkdownTextManagerInterface<T extends View> extends Vi
39
40
  void setTextBreakStrategy(T view, @Nullable String value);
40
41
  void setLineBreakStrategyIOS(T view, @Nullable String value);
41
42
  void setWritingDirection(T view, @Nullable String value);
43
+ void clearSelection(T view);
42
44
  }
@@ -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};
@@ -26,13 +26,18 @@ import com.swmansion.enriched.markdown.utils.common.SegmentReconciler
26
26
  import com.swmansion.enriched.markdown.utils.common.StreamingMarkdownFilter
27
27
  import com.swmansion.enriched.markdown.utils.common.TableStreamingMode
28
28
  import com.swmansion.enriched.markdown.utils.common.emitLinkLayout
29
+ import com.swmansion.enriched.markdown.utils.common.emitSelectionChange
29
30
  import com.swmansion.enriched.markdown.utils.common.isReducedMotionEnabled
30
31
  import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments
31
32
  import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator
32
33
  import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutCollector
33
34
  import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutFragment
35
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionChangeReporter
34
36
  import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
37
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionRect
38
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionSnapshot
35
39
  import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors
40
+ import com.swmansion.enriched.markdown.utils.text.view.currentTextSelection
36
41
  import com.swmansion.enriched.markdown.views.BlockMenuOptions
37
42
  import com.swmansion.enriched.markdown.views.BlockSegmentView
38
43
  import com.swmansion.enriched.markdown.views.CodeBlockContainerView
@@ -105,6 +110,21 @@ class EnrichedMarkdown
105
110
  private var linkLayoutEnabled = false
106
111
  private var lastLinkLayout: List<LinkLayoutFragment>? = null
107
112
 
113
+ // At most one segment holds a selection; a new one clears the previous.
114
+ private var selectionOwner: EnrichedMarkdownInternalText? = null
115
+
116
+ // A block long-pressed while contextMenuHidden is on, reported as the selection.
117
+ private class ActiveBlock(
118
+ val view: View,
119
+ val source: String,
120
+ val markdown: String,
121
+ val text: String,
122
+ )
123
+
124
+ private var activeBlock: ActiveBlock? = null
125
+ private val selectionReporter =
126
+ SelectionChangeReporter(this, ::selectionSnapshot) { snapshot -> emitSelectionChange(this, snapshot) }
127
+
108
128
  private var onLinkPressCallback: ((String) -> Unit)? = null
109
129
  private var onLinkLongPressCallback: ((String) -> Unit)? = null
110
130
  private var onTaskListItemPressCallback: ((Int, Boolean, String) -> Unit)? = null
@@ -398,6 +418,78 @@ class EnrichedMarkdown
398
418
  onContextMenuItemPressCallback?.invoke(itemText, selectedText, selectionStart, selectionEnd)
399
419
  }
400
420
 
421
+ fun setEmitSelectionChange(enabled: Boolean) {
422
+ selectionReporter.enabled = enabled
423
+ }
424
+
425
+ /** Clears the selection of every segment (and any active block) and reports `active: false`. */
426
+ fun clearSelection() {
427
+ val owner = selectionOwner
428
+ selectionOwner = null
429
+ activeBlock = null
430
+ owner?.clearSelection()
431
+ selectionReporter.flushNow()
432
+ }
433
+
434
+ private fun onSegmentSelectionChanged(
435
+ view: EnrichedMarkdownInternalText,
436
+ hasSelection: Boolean,
437
+ ) {
438
+ if (hasSelection) {
439
+ // Take ownership first so the previous owner's own "cleared" callback is ignored.
440
+ val previous = selectionOwner
441
+ selectionOwner = view
442
+ activeBlock = null
443
+ if (previous !== view) previous?.clearSelection()
444
+ selectionReporter.schedule()
445
+ } else if (selectionOwner === view) {
446
+ selectionReporter.onSelectionChanged(false)
447
+ }
448
+ }
449
+
450
+ private fun onBlockMenuSuppressed(
451
+ view: View,
452
+ source: String,
453
+ markdown: String,
454
+ text: String,
455
+ ) {
456
+ if (!selectionReporter.enabled) return
457
+ val previous = selectionOwner
458
+ selectionOwner = null
459
+ previous?.clearSelection()
460
+ activeBlock = ActiveBlock(view, source, markdown, text)
461
+ selectionReporter.forgetLastReported()
462
+ selectionReporter.flushNow()
463
+ }
464
+
465
+ private fun selectionSnapshot(): SelectionSnapshot {
466
+ activeBlock?.let { block ->
467
+ if (block.view.parent !== this) return SelectionSnapshot.INACTIVE
468
+ val rect =
469
+ SelectionRect.fromPx(
470
+ block.view.left.toFloat(),
471
+ block.view.top.toFloat(),
472
+ block.view.right.toFloat(),
473
+ block.view.bottom.toFloat(),
474
+ )
475
+ return SelectionSnapshot.block(block.source, block.markdown, block.text, rect)
476
+ }
477
+ val owner = selectionOwner?.takeIf { it.parent === this } ?: return SelectionSnapshot.INACTIVE
478
+ val selection = owner.currentTextSelection(owner.left, owner.top) ?: return SelectionSnapshot.INACTIVE
479
+ // Same rule as forwardSegmentCopyRequest.
480
+ val whole = selection.fullSelection && segmentViews.size == 1 && currentMarkdown.isNotEmpty()
481
+ return SelectionSnapshot(
482
+ active = true,
483
+ source = "selection",
484
+ text = selection.text,
485
+ markdown = if (whole) currentMarkdown else selection.markdown,
486
+ whole = whole,
487
+ rect = selection.rect,
488
+ startRect = selection.startRect,
489
+ endRect = selection.endRect,
490
+ )
491
+ }
492
+
401
493
  private fun recreateStyleConfig() {
402
494
  markdownStyleMap?.let {
403
495
  markdownStyle =
@@ -498,6 +590,9 @@ class EnrichedMarkdown
498
590
  segmentSignatures.clear()
499
591
  segmentSignatures.addAll(result.signatures)
500
592
 
593
+ if (selectionOwner?.let { owner -> segmentViews.none { it === owner } } == true) selectionOwner = null
594
+ if (activeBlock?.let { block -> segmentViews.none { it === block.view } } == true) activeBlock = null
595
+
501
596
  // A just-closed block has unchanged content, so the reconciler reuses it
502
597
  // without an update; sync pending here to trigger its deferred highlight.
503
598
  segmentViews.forEachIndexed { index, view ->
@@ -637,6 +732,7 @@ class EnrichedMarkdown
637
732
  this@EnrichedMarkdown.onTaskListItemPressCallback?.invoke(taskIndex, checked, itemText)
638
733
  }
639
734
  onCopyRequest = ::forwardSegmentCopyRequest
735
+ onSelectionChangedCallback = ::onSegmentSelectionChanged
640
736
 
641
737
  if (contextMenuItemTexts.isNotEmpty()) {
642
738
  setContextMenuItems(contextMenuItemTexts, ::forwardContextMenuItemPress)
@@ -658,6 +754,7 @@ class EnrichedMarkdown
658
754
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
659
755
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
660
756
  menuOptions = blockMenuOptions("table")
757
+ onMenuSuppressed = { markdown, text -> onBlockMenuSuppressed(this, "table", markdown, text) }
661
758
  applyTableNode(segment.node)
662
759
  }
663
760
 
@@ -669,6 +766,7 @@ class EnrichedMarkdown
669
766
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
670
767
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
671
768
  menuOptions = blockMenuOptions("code")
769
+ onMenuSuppressed = { markdown, text -> onBlockMenuSuppressed(this, "code", markdown, text) }
672
770
  onCopyPress = { code, language ->
673
771
  this@EnrichedMarkdown.onCopyPressCallback?.invoke(code, language)
674
772
  }
@@ -702,6 +800,12 @@ class EnrichedMarkdown
702
800
  .getMethod("setMenuOptions", BlockMenuOptions::class.java)
703
801
  .invoke(view, blockMenuOptions("math"))
704
802
  setMathContextMenuHidden(view)
803
+ val onMenuSuppressed: (String, String) -> Unit = { markdown, text ->
804
+ onBlockMenuSuppressed(view, "math", markdown, text)
805
+ }
806
+ runCatching {
807
+ resolvedClass.getMethod("setOnMenuSuppressed", Function2::class.java).invoke(view, onMenuSuppressed)
808
+ }.onFailure { Log.e(TAG, "Failed to set onMenuSuppressed on math view", it) }
705
809
  resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
706
810
  view
707
811
  } catch (e: Exception) {
@@ -789,6 +893,8 @@ class EnrichedMarkdown
789
893
  }
790
894
 
791
895
  emitLinkLayoutIfNeeded()
896
+ // Segments may have moved or been replaced; re-read the selection (deduplicated).
897
+ if (selectionOwner != null || activeBlock != null) selectionReporter.schedule() else selectionReporter.flushNow()
792
898
  }
793
899
 
794
900
  fun setEmitLinkLayout(enabled: Boolean) {
@@ -837,6 +943,7 @@ class EnrichedMarkdown
837
943
  }
838
944
 
839
945
  fun cleanup() {
946
+ selectionReporter.cancel()
840
947
  executor.shutdownNow()
841
948
  }
842
949
 
@@ -18,6 +18,7 @@ import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
18
18
  import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
19
19
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
20
20
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
21
+ import com.swmansion.enriched.markdown.utils.text.view.clearTextSelection
21
22
  import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback
22
23
  import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
23
24
  import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
@@ -57,6 +58,10 @@ class EnrichedMarkdownInternalText
57
58
  private var onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null
58
59
  var selectionMenuConfig: SelectionMenuConfig = SelectionMenuConfig()
59
60
  var onCopyRequest: ((markdown: String, text: String, fullSelection: Boolean) -> Unit)? = null
61
+
62
+ // Read from onSelectionChanged, which TextView already calls from its own
63
+ // constructor — so it must stay nullable.
64
+ var onSelectionChangedCallback: ((view: EnrichedMarkdownInternalText, hasSelection: Boolean) -> Unit)? = null
60
65
  private var activeActionMode: ActionMode? = null
61
66
  var contextMenuHidden: Boolean = false
62
67
  set(value) {
@@ -115,6 +120,20 @@ class EnrichedMarkdownInternalText
115
120
  applySelectableState(selectable)
116
121
  }
117
122
 
123
+ override fun onSelectionChanged(
124
+ selStart: Int,
125
+ selEnd: Int,
126
+ ) {
127
+ super.onSelectionChanged(selStart, selEnd)
128
+ onSelectionChangedCallback?.invoke(this, selEnd > selStart)
129
+ }
130
+
131
+ /** Clears the selection and ends its ActionMode. */
132
+ fun clearSelection() {
133
+ clearTextSelection()
134
+ activeActionMode?.finish()
135
+ }
136
+
118
137
  fun setContextMenuItems(
119
138
  items: List<String>,
120
139
  onPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit,
@@ -185,6 +185,20 @@ class EnrichedMarkdownManager :
185
185
  view?.setEmitLinkLayout(emitLinkLayout)
186
186
  }
187
187
 
188
+ @ReactProp(name = "emitSelectionChange", defaultBoolean = false)
189
+ override fun setEmitSelectionChange(
190
+ view: EnrichedMarkdown?,
191
+ emitSelectionChange: Boolean,
192
+ ) {
193
+ view?.setEmitSelectionChange(emitSelectionChange)
194
+ }
195
+
196
+ // Commands
197
+
198
+ override fun clearSelection(view: EnrichedMarkdown?) {
199
+ view?.clearSelection()
200
+ }
201
+
188
202
  @ReactProp(name = "enableLinkPreview", defaultBoolean = true)
189
203
  override fun setEnableLinkPreview(
190
204
  view: EnrichedMarkdown?,
@@ -25,17 +25,22 @@ import com.swmansion.enriched.markdown.spoiler.SpoilerOverlayDrawer
25
25
  import com.swmansion.enriched.markdown.styles.StyleConfig
26
26
  import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils
27
27
  import com.swmansion.enriched.markdown.utils.common.emitLinkLayout
28
+ import com.swmansion.enriched.markdown.utils.common.emitSelectionChange
28
29
  import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator
29
30
  import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper
30
31
  import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutCollector
31
32
  import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutFragment
32
33
  import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
34
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionChangeReporter
33
35
  import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
36
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionSnapshot
34
37
  import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
35
38
  import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors
36
39
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
37
40
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
41
+ import com.swmansion.enriched.markdown.utils.text.view.clearTextSelection
38
42
  import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback
43
+ import com.swmansion.enriched.markdown.utils.text.view.currentTextSelection
39
44
  import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent
40
45
  import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent
41
46
  import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
@@ -108,8 +113,14 @@ class EnrichedMarkdownText
108
113
  private var linkLayoutEnabled = false
109
114
  private var lastLinkLayout: List<LinkLayoutFragment>? = null
110
115
 
116
+ // Nullable and assigned in init: TextView calls onSelectionChanged from its
117
+ // own constructor, before this class's fields exist.
118
+ private var selectionReporter: SelectionChangeReporter? = null
119
+
111
120
  init {
112
121
  setupAsMarkdownTextView()
122
+ selectionReporter =
123
+ SelectionChangeReporter(this, ::selectionSnapshot) { snapshot -> emitSelectionChange(this, snapshot) }
113
124
  customSelectionActionModeCallback =
114
125
  createSelectionActionModeCallback(
115
126
  this,
@@ -317,6 +328,42 @@ class EnrichedMarkdownText
317
328
 
318
329
  applySelectionColors(selectionColor, selectionHandleColor)
319
330
  emitLinkLayoutIfNeeded()
331
+ // New text drops the selection; report it if setText didn't already.
332
+ selectionReporter?.schedule()
333
+ }
334
+
335
+ fun setEmitSelectionChange(enabled: Boolean) {
336
+ selectionReporter?.enabled = enabled
337
+ }
338
+
339
+ /** Clears the selection (ending its ActionMode) and reports `active: false`. */
340
+ fun clearSelection() {
341
+ clearTextSelection()
342
+ activeActionMode?.finish()
343
+ selectionReporter?.flushNow()
344
+ }
345
+
346
+ override fun onSelectionChanged(
347
+ selStart: Int,
348
+ selEnd: Int,
349
+ ) {
350
+ super.onSelectionChanged(selStart, selEnd)
351
+ selectionReporter?.onSelectionChanged(selEnd > selStart)
352
+ }
353
+
354
+ private fun selectionSnapshot(): SelectionSnapshot {
355
+ val selection = currentTextSelection() ?: return SelectionSnapshot.INACTIVE
356
+ return SelectionSnapshot(
357
+ active = true,
358
+ source = "selection",
359
+ text = selection.text,
360
+ markdown = selection.markdown,
361
+ // Same rule as onCopyRequest: a full selection already carries the original markdown.
362
+ whole = selection.fullSelection && currentMarkdown.isNotEmpty(),
363
+ rect = selection.rect,
364
+ startRect = selection.startRect,
365
+ endRect = selection.endRect,
366
+ )
320
367
  }
321
368
 
322
369
  fun setEmitLinkLayout(enabled: Boolean) {
@@ -424,6 +471,7 @@ class EnrichedMarkdownText
424
471
  }
425
472
 
426
473
  override fun onDetachedFromWindow() {
474
+ selectionReporter?.cancel()
427
475
  stopSpoilerAnimations()
428
476
  super.onDetachedFromWindow()
429
477
  }
@@ -185,6 +185,20 @@ class EnrichedMarkdownTextManager :
185
185
  view?.setEmitLinkLayout(emitLinkLayout)
186
186
  }
187
187
 
188
+ @ReactProp(name = "emitSelectionChange", defaultBoolean = false)
189
+ override fun setEmitSelectionChange(
190
+ view: EnrichedMarkdownText?,
191
+ emitSelectionChange: Boolean,
192
+ ) {
193
+ view?.setEmitSelectionChange(emitSelectionChange)
194
+ }
195
+
196
+ // Commands
197
+
198
+ override fun clearSelection(view: EnrichedMarkdownText?) {
199
+ view?.clearSelection()
200
+ }
201
+
188
202
  @ReactProp(name = "enableLinkPreview", defaultBoolean = true)
189
203
  override fun setEnableLinkPreview(
190
204
  view: EnrichedMarkdownText?,