react-native-enriched-markdown-scaffold 1.0.2-scaffold.1 → 1.0.2-scaffold.10

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 (163) hide show
  1. package/android/build.gradle +2 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +18 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +4 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +18 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +4 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +30 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +210 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +176 -2
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +45 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +35 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +112 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +34 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +46 -8
  16. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
  18. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +43 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +25 -3
  20. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +7 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
  22. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
  23. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
  24. package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
  27. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +16 -1
  28. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
  29. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
  30. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +3 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +51 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +34 -2
  33. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
  34. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/HeadlessActionMode.kt +88 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +53 -14
  38. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +232 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +64 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +29 -3
  41. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +30 -7
  42. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +1 -0
  43. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +13 -6
  44. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathMeasureHelper.kt +2 -1
  45. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +25 -9
  46. package/docs/API_REFERENCE.md +183 -4
  47. package/docs/COPY_OPTIONS.md +40 -0
  48. package/docs/INPUT.md +3 -1
  49. package/docs/MENTIONS.md +8 -1
  50. package/docs/STYLES.md +27 -0
  51. package/docs/TEXT.md +2 -0
  52. package/docs/WEB.md +1 -0
  53. package/ios/EnrichedMarkdown.mm +321 -9
  54. package/ios/EnrichedMarkdownText.mm +203 -6
  55. package/ios/attachments/ENRMMathInlineAttachment+macOS.m +2 -1
  56. package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
  57. package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
  58. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
  59. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
  60. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +30 -0
  61. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +210 -0
  62. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  63. package/ios/internals/MeasurementCache.h +8 -2
  64. package/ios/math/ENRMRaTeXBridge.swift +23 -5
  65. package/ios/renderer/ENRMMathInlineRenderer.m +1 -0
  66. package/ios/renderer/LinkRenderer.m +47 -15
  67. package/ios/styles/StyleConfig.h +20 -0
  68. package/ios/styles/StyleConfig.mm +84 -0
  69. package/ios/utils/CodeBackground.m +3 -65
  70. package/ios/utils/ENRMBlockMenu.h +66 -0
  71. package/ios/utils/ENRMChipDrawing.h +24 -0
  72. package/ios/utils/ENRMChipDrawing.m +83 -0
  73. package/ios/utils/ENRMLinkChip.h +70 -0
  74. package/ios/utils/ENRMLinkChip.m +213 -0
  75. package/ios/utils/ENRMLinkLayout.h +50 -0
  76. package/ios/utils/ENRMLinkLayout.m +45 -0
  77. package/ios/utils/ENRMSelectionChange.h +120 -0
  78. package/ios/utils/ENRMSelectionChange.m +282 -0
  79. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  80. package/ios/utils/EditMenuUtils.h +35 -6
  81. package/ios/utils/EditMenuUtils.m +171 -15
  82. package/ios/utils/HTMLGenerator.m +2 -1
  83. package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
  84. package/ios/utils/MarkdownExtractor.m +2 -1
  85. package/ios/utils/RTFExportUtils.m +2 -0
  86. package/ios/utils/StylePropsUtils.h +47 -1
  87. package/ios/utils/TextViewLayoutManager.mm +3 -0
  88. package/ios/views/ENRMCodeBlockContainerView.h +16 -2
  89. package/ios/views/ENRMCodeBlockContainerView.m +22 -19
  90. package/ios/views/ENRMMathContainerView.h +11 -0
  91. package/ios/views/ENRMMathContainerView.m +17 -20
  92. package/ios/views/TableContainerView.h +14 -0
  93. package/ios/views/TableContainerView.m +34 -18
  94. package/lib/module/EnrichedMarkdownNativeComponent.ts +138 -1
  95. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +138 -1
  96. package/lib/module/index.js.map +1 -1
  97. package/lib/module/jest/index.js +14 -0
  98. package/lib/module/jest/index.js.map +1 -1
  99. package/lib/module/native/EnrichedMarkdownText.js +169 -11
  100. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  101. package/lib/module/normalizeMarkdownStyle.js +17 -4
  102. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  103. package/lib/module/normalizeMarkdownStyle.web.js +17 -4
  104. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  105. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  106. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  107. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  108. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  109. package/lib/module/web/styles.js +18 -0
  110. package/lib/module/web/styles.js.map +1 -1
  111. package/lib/module/web/utils.js +13 -0
  112. package/lib/module/web/utils.js.map +1 -1
  113. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +123 -2
  114. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  115. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +123 -2
  116. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  117. package/lib/typescript/src/index.d.ts +2 -2
  118. package/lib/typescript/src/index.d.ts.map +1 -1
  119. package/lib/typescript/src/index.web.d.ts +1 -1
  120. package/lib/typescript/src/index.web.d.ts.map +1 -1
  121. package/lib/typescript/src/jest/index.d.ts +1 -1
  122. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  123. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  124. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  125. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  126. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  127. package/lib/typescript/src/types/MarkdownStyle.d.ts +50 -5
  128. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  129. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +12 -0
  130. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  131. package/lib/typescript/src/types/MarkdownTextProps.d.ts +144 -11
  132. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  133. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  134. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  135. package/lib/typescript/src/types/events.d.ts +66 -0
  136. package/lib/typescript/src/types/events.d.ts.map +1 -1
  137. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  138. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  139. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  140. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  141. package/lib/typescript/src/web/types.d.ts +4 -0
  142. package/lib/typescript/src/web/types.d.ts.map +1 -1
  143. package/lib/typescript/src/web/utils.d.ts +4 -0
  144. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  145. package/package.json +1 -1
  146. package/src/EnrichedMarkdownNativeComponent.ts +138 -1
  147. package/src/EnrichedMarkdownTextNativeComponent.ts +138 -1
  148. package/src/index.tsx +6 -0
  149. package/src/index.web.tsx +5 -1
  150. package/src/jest/index.tsx +22 -1
  151. package/src/native/EnrichedMarkdownText.tsx +220 -9
  152. package/src/normalizeMarkdownStyle.ts +15 -0
  153. package/src/normalizeMarkdownStyle.web.ts +13 -0
  154. package/src/types/MarkdownStyle.ts +51 -5
  155. package/src/types/MarkdownStyleInternal.ts +12 -0
  156. package/src/types/MarkdownTextProps.ts +151 -11
  157. package/src/types/MarkdownTextProps.web.ts +43 -1
  158. package/src/types/events.ts +69 -0
  159. package/src/web/EnrichedMarkdownText.tsx +10 -3
  160. package/src/web/renderers/InlineRenderers.tsx +111 -3
  161. package/src/web/styles.ts +25 -0
  162. package/src/web/types.ts +4 -0
  163. package/src/web/utils.ts +8 -0
@@ -25,11 +25,20 @@ import com.swmansion.enriched.markdown.utils.common.RenderedSegment
25
25
  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
+ import com.swmansion.enriched.markdown.utils.common.emitLinkLayout
29
+ import com.swmansion.enriched.markdown.utils.common.emitSelectionChange
28
30
  import com.swmansion.enriched.markdown.utils.common.isReducedMotionEnabled
29
31
  import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments
30
32
  import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator
33
+ import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutCollector
34
+ import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutFragment
35
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionChangeReporter
31
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
32
39
  import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors
40
+ import com.swmansion.enriched.markdown.utils.text.view.currentTextSelection
41
+ import com.swmansion.enriched.markdown.views.BlockMenuOptions
33
42
  import com.swmansion.enriched.markdown.views.BlockSegmentView
34
43
  import com.swmansion.enriched.markdown.views.CodeBlockContainerView
35
44
  import com.swmansion.enriched.markdown.views.TableContainerView
@@ -98,6 +107,23 @@ class EnrichedMarkdown
98
107
  private var selectionMenuConfig = SelectionMenuConfig()
99
108
  private var accessibilityLabels = AccessibilityLabels()
100
109
  private var textBreakStrategy: String = BreakStrategyUtils.DEFAULT_STRATEGY
110
+ private var linkLayoutEnabled = false
111
+ private var lastLinkLayout: List<LinkLayoutFragment>? = null
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) }
101
127
 
102
128
  private var onLinkPressCallback: ((String) -> Unit)? = null
103
129
  private var onLinkLongPressCallback: ((String) -> Unit)? = null
@@ -105,6 +131,7 @@ class EnrichedMarkdown
105
131
  private var onCopyPressCallback: ((String, String) -> Unit)? = null
106
132
  private var contextMenuItemTexts: List<String> = emptyList()
107
133
  var onContextMenuItemPressCallback: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null
134
+ var onCopyRequestCallback: ((source: String, markdown: String, text: String, whole: Boolean) -> Unit)? = null
108
135
  var spoilerOverlay: SpoilerOverlay = SpoilerOverlay.PARTICLES
109
136
  set(value) {
110
137
  if (field == value) return
@@ -309,8 +336,8 @@ class EnrichedMarkdown
309
336
  segmentViews.filterIsInstance<EnrichedMarkdownInternalText>().forEach {
310
337
  it.selectionMenuConfig = config
311
338
  }
312
- // Table and math views cache the copy labels, so re-push them on update
313
- // (e.g. a language change without a remount) to avoid stale labels.
339
+ // Block views cache the copy labels and menu options, so re-push them on
340
+ // update (e.g. a language change without a remount) to avoid stale values.
314
341
  pushCopyLabelsToBlockSegments()
315
342
  }
316
343
 
@@ -322,11 +349,13 @@ class EnrichedMarkdown
322
349
  view is TableContainerView -> {
323
350
  view.copyLabel = copyLabel
324
351
  view.copyAsMarkdownLabel = copyAsMarkdownLabel
352
+ view.menuOptions = blockMenuOptions("table")
325
353
  }
326
354
 
327
355
  view is CodeBlockContainerView -> {
328
356
  view.copyLabel = copyLabel
329
357
  view.copyAsMarkdownLabel = copyAsMarkdownLabel
358
+ view.menuOptions = blockMenuOptions("code")
330
359
  }
331
360
 
332
361
  isMathContainerView(view) -> {
@@ -335,6 +364,9 @@ class EnrichedMarkdown
335
364
  view.javaClass
336
365
  .getMethod("setCopyAsMarkdownLabel", String::class.java)
337
366
  .invoke(view, copyAsMarkdownLabel)
367
+ view.javaClass
368
+ .getMethod("setMenuOptions", BlockMenuOptions::class.java)
369
+ .invoke(view, blockMenuOptions("math"))
338
370
  }
339
371
  }
340
372
  }
@@ -361,6 +393,22 @@ class EnrichedMarkdown
361
393
  }
362
394
  }
363
395
 
396
+ private fun blockMenuOptions(source: String) =
397
+ BlockMenuOptions.from(selectionMenuConfig) { markdown, text ->
398
+ onCopyRequestCallback?.invoke(source, markdown, text, true)
399
+ }
400
+
401
+ // Segments only see their own text, so a selection is the whole component
402
+ // only when it spans the sole segment; the markdown is then the original.
403
+ private fun forwardSegmentCopyRequest(
404
+ markdown: String,
405
+ text: String,
406
+ fullSelection: Boolean,
407
+ ) {
408
+ val whole = fullSelection && segmentViews.size == 1 && currentMarkdown.isNotEmpty()
409
+ onCopyRequestCallback?.invoke("selection", if (whole) currentMarkdown else markdown, text, whole)
410
+ }
411
+
364
412
  private fun forwardContextMenuItemPress(
365
413
  itemText: String,
366
414
  selectedText: String,
@@ -370,6 +418,78 @@ class EnrichedMarkdown
370
418
  onContextMenuItemPressCallback?.invoke(itemText, selectedText, selectionStart, selectionEnd)
371
419
  }
372
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
+
373
493
  private fun recreateStyleConfig() {
374
494
  markdownStyleMap?.let {
375
495
  markdownStyle =
@@ -470,6 +590,9 @@ class EnrichedMarkdown
470
590
  segmentSignatures.clear()
471
591
  segmentSignatures.addAll(result.signatures)
472
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
+
473
596
  // A just-closed block has unchanged content, so the reconciler reuses it
474
597
  // without an update; sync pending here to trigger its deferred highlight.
475
598
  segmentViews.forEachIndexed { index, view ->
@@ -608,6 +731,8 @@ class EnrichedMarkdown
608
731
  onTaskListItemPressCallback = { taskIndex, checked, itemText ->
609
732
  this@EnrichedMarkdown.onTaskListItemPressCallback?.invoke(taskIndex, checked, itemText)
610
733
  }
734
+ onCopyRequest = ::forwardSegmentCopyRequest
735
+ onSelectionChangedCallback = ::onSegmentSelectionChanged
611
736
 
612
737
  if (contextMenuItemTexts.isNotEmpty()) {
613
738
  setContextMenuItems(contextMenuItemTexts, ::forwardContextMenuItemPress)
@@ -628,6 +753,8 @@ class EnrichedMarkdown
628
753
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
629
754
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
630
755
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
756
+ menuOptions = blockMenuOptions("table")
757
+ onMenuSuppressed = { markdown, text -> onBlockMenuSuppressed(this, "table", markdown, text) }
631
758
  applyTableNode(segment.node)
632
759
  }
633
760
 
@@ -638,6 +765,8 @@ class EnrichedMarkdown
638
765
  copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
639
766
  copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
640
767
  contextMenuHidden = this@EnrichedMarkdown.contextMenuHidden
768
+ menuOptions = blockMenuOptions("code")
769
+ onMenuSuppressed = { markdown, text -> onBlockMenuSuppressed(this, "code", markdown, text) }
641
770
  onCopyPress = { code, language ->
642
771
  this@EnrichedMarkdown.onCopyPressCallback?.invoke(code, language)
643
772
  }
@@ -667,7 +796,16 @@ class EnrichedMarkdown
667
796
  resolvedClass
668
797
  .getMethod("setCopyAsMarkdownLabel", String::class.java)
669
798
  .invoke(view, selectionMenuConfig.copyAsMarkdownLabel)
799
+ resolvedClass
800
+ .getMethod("setMenuOptions", BlockMenuOptions::class.java)
801
+ .invoke(view, blockMenuOptions("math"))
670
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) }
671
809
  resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
672
810
  view
673
811
  } catch (e: Exception) {
@@ -753,6 +891,41 @@ class EnrichedMarkdown
753
891
  currentY += segment?.segmentMarginBottom ?: 0
754
892
  }
755
893
  }
894
+
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()
898
+ }
899
+
900
+ fun setEmitLinkLayout(enabled: Boolean) {
901
+ if (linkLayoutEnabled == enabled) return
902
+ linkLayoutEnabled = enabled
903
+ lastLinkLayout = null
904
+ emitLinkLayoutIfNeeded()
905
+ }
906
+
907
+ /**
908
+ * Reports link rects of every text segment (offset by the segment's position) to JS for
909
+ * `renderLinkDecoration`. Tables are skipped: their cells scroll horizontally, so rects would
910
+ * go stale. Runs after each segment layout pass; deduplicated so an unchanged layout sends nothing.
911
+ */
912
+ private fun emitLinkLayoutIfNeeded() {
913
+ if (!linkLayoutEnabled || width <= 0) return
914
+ val fragments = mutableListOf<LinkLayoutFragment>()
915
+ var linkCount = 0
916
+ segmentViews.forEach { view ->
917
+ if (view is TableContainerView) {
918
+ linkCount += view.linkCount
919
+ return@forEach
920
+ }
921
+ if (view !is EnrichedMarkdownInternalText) return@forEach
922
+ val (segmentFragments, segmentLinks) = LinkLayoutCollector.collect(view, view.left, view.top, linkCount)
923
+ fragments += segmentFragments
924
+ linkCount += segmentLinks
925
+ }
926
+ if (fragments == lastLinkLayout) return
927
+ lastLinkLayout = fragments
928
+ emitLinkLayout(this, fragments)
756
929
  }
757
930
 
758
931
  private fun computeSegmentsTotalHeight(): Int {
@@ -770,6 +943,7 @@ class EnrichedMarkdown
770
943
  }
771
944
 
772
945
  fun cleanup() {
946
+ selectionReporter.cancel()
773
947
  executor.shutdownNow()
774
948
  }
775
949
 
@@ -13,12 +13,15 @@ import com.swmansion.enriched.markdown.spoiler.SpoilerCapable
13
13
  import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay
14
14
  import com.swmansion.enriched.markdown.spoiler.SpoilerOverlayDrawer
15
15
  import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper
16
+ import com.swmansion.enriched.markdown.utils.text.view.HeadlessActionMode
16
17
  import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
17
18
  import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
18
19
  import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
19
20
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
20
21
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
22
+ import com.swmansion.enriched.markdown.utils.text.view.clearTextSelection
21
23
  import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback
24
+ import com.swmansion.enriched.markdown.utils.text.view.isRomSelectionPopupShower
22
25
  import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
23
26
  import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
24
27
  import com.swmansion.enriched.markdown.views.BlockSegmentView
@@ -56,6 +59,11 @@ class EnrichedMarkdownInternalText
56
59
  private var contextMenuItemTexts: List<String> = emptyList()
57
60
  private var onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null
58
61
  var selectionMenuConfig: SelectionMenuConfig = SelectionMenuConfig()
62
+ var onCopyRequest: ((markdown: String, text: String, fullSelection: Boolean) -> Unit)? = null
63
+
64
+ // Read from onSelectionChanged, which TextView already calls from its own
65
+ // constructor — so it must stay nullable.
66
+ var onSelectionChangedCallback: ((view: EnrichedMarkdownInternalText, hasSelection: Boolean) -> Unit)? = null
59
67
  private var activeActionMode: ActionMode? = null
60
68
  var contextMenuHidden: Boolean = false
61
69
  set(value) {
@@ -82,6 +90,9 @@ class EnrichedMarkdownInternalText
82
90
  onCustomItemPress = { itemText, selectedText, start, end ->
83
91
  onContextMenuItemPress?.invoke(itemText, selectedText, start, end)
84
92
  },
93
+ onCopyRequest = { markdown, text, fullSelection ->
94
+ onCopyRequest?.invoke(markdown, text, fullSelection)
95
+ },
85
96
  )
86
97
  }
87
98
 
@@ -111,6 +122,40 @@ class EnrichedMarkdownInternalText
111
122
  applySelectableState(selectable)
112
123
  }
113
124
 
125
+ override fun onSelectionChanged(
126
+ selStart: Int,
127
+ selEnd: Int,
128
+ ) {
129
+ super.onSelectionChanged(selStart, selEnd)
130
+ onSelectionChangedCallback?.invoke(this, selEnd > selStart)
131
+ }
132
+
133
+ // See HeadlessActionMode: with the menu hidden, no toolbar window is created.
134
+ override fun startActionMode(
135
+ callback: ActionMode.Callback?,
136
+ type: Int,
137
+ ): ActionMode? {
138
+ if (contextMenuHidden && type == ActionMode.TYPE_FLOATING && callback != null) {
139
+ return HeadlessActionMode.start(this, callback)
140
+ }
141
+ return super.startActionMode(callback, type)
142
+ }
143
+
144
+ // See isRomSelectionPopupShower: MIUI shows its own selection popup this way.
145
+ override fun postDelayed(
146
+ action: Runnable?,
147
+ delayMillis: Long,
148
+ ): Boolean {
149
+ if (contextMenuHidden && isRomSelectionPopupShower(action)) return false
150
+ return super.postDelayed(action, delayMillis)
151
+ }
152
+
153
+ /** Clears the selection and ends its ActionMode. */
154
+ fun clearSelection() {
155
+ clearTextSelection()
156
+ activeActionMode?.finish()
157
+ }
158
+
114
159
  fun setContextMenuItems(
115
160
  items: List<String>,
116
161
  onPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit,
@@ -18,6 +18,7 @@ import com.swmansion.enriched.markdown.utils.common.CodeBlockStreamingMode
18
18
  import com.swmansion.enriched.markdown.utils.common.TableStreamingMode
19
19
  import com.swmansion.enriched.markdown.utils.common.emitContextMenuItemPress
20
20
  import com.swmansion.enriched.markdown.utils.common.emitCopyPress
21
+ import com.swmansion.enriched.markdown.utils.common.emitCopyRequest
21
22
  import com.swmansion.enriched.markdown.utils.common.emitLinkLongPress
22
23
  import com.swmansion.enriched.markdown.utils.common.emitLinkPress
23
24
  import com.swmansion.enriched.markdown.utils.common.emitTaskListItemPress
@@ -65,6 +66,10 @@ class EnrichedMarkdownManager :
65
66
  emitCopyPress(view, code, language)
66
67
  }
67
68
 
69
+ view.onCopyRequestCallback = { source, markdown, text, whole ->
70
+ emitCopyRequest(view, source, markdown, text, whole)
71
+ }
72
+
68
73
  return view
69
74
  }
70
75
 
@@ -172,6 +177,28 @@ class EnrichedMarkdownManager :
172
177
  view?.setContextMenuHidden(contextMenuHidden)
173
178
  }
174
179
 
180
+ @ReactProp(name = "emitLinkLayout", defaultBoolean = false)
181
+ override fun setEmitLinkLayout(
182
+ view: EnrichedMarkdown?,
183
+ emitLinkLayout: Boolean,
184
+ ) {
185
+ view?.setEmitLinkLayout(emitLinkLayout)
186
+ }
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
+
175
202
  @ReactProp(name = "enableLinkPreview", defaultBoolean = true)
176
203
  override fun setEnableLinkPreview(
177
204
  view: EnrichedMarkdown?,
@@ -180,6 +207,14 @@ class EnrichedMarkdownManager :
180
207
  // No-op on Android — only used on iOS
181
208
  }
182
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
+
183
218
  @ReactProp(name = "enableTaskListItemToggle", defaultBoolean = true)
184
219
  override fun setEnableTaskListItemToggle(
185
220
  view: EnrichedMarkdown?,
@@ -24,17 +24,27 @@ import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay
24
24
  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
+ import com.swmansion.enriched.markdown.utils.common.emitLinkLayout
28
+ import com.swmansion.enriched.markdown.utils.common.emitSelectionChange
27
29
  import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator
28
30
  import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper
31
+ import com.swmansion.enriched.markdown.utils.text.view.HeadlessActionMode
32
+ import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutCollector
33
+ import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutFragment
29
34
  import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
35
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionChangeReporter
30
36
  import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
37
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionSnapshot
31
38
  import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
32
39
  import com.swmansion.enriched.markdown.utils.text.view.applySelectionColors
33
40
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
34
41
  import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
42
+ import com.swmansion.enriched.markdown.utils.text.view.clearTextSelection
35
43
  import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback
44
+ import com.swmansion.enriched.markdown.utils.text.view.currentTextSelection
36
45
  import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent
37
46
  import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent
47
+ import com.swmansion.enriched.markdown.utils.text.view.isRomSelectionPopupShower
38
48
  import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
39
49
  import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
40
50
  import java.util.concurrent.Executors
@@ -68,6 +78,7 @@ class EnrichedMarkdownText
68
78
 
69
79
  private var contextMenuItemTexts: List<String> = emptyList()
70
80
  var onContextMenuItemPressCallback: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null
81
+ var onCopyRequestCallback: ((markdown: String, text: String, whole: Boolean) -> Unit)? = null
71
82
 
72
83
  var markdownStyle: StyleConfig? = null
73
84
  private set
@@ -101,8 +112,17 @@ class EnrichedMarkdownText
101
112
  private var contextMenuHidden = false
102
113
  private var activeActionMode: ActionMode? = null
103
114
 
115
+ private var linkLayoutEnabled = false
116
+ private var lastLinkLayout: List<LinkLayoutFragment>? = null
117
+
118
+ // Nullable and assigned in init: TextView calls onSelectionChanged from its
119
+ // own constructor, before this class's fields exist.
120
+ private var selectionReporter: SelectionChangeReporter? = null
121
+
104
122
  init {
105
123
  setupAsMarkdownTextView()
124
+ selectionReporter =
125
+ SelectionChangeReporter(this, ::selectionSnapshot) { snapshot -> emitSelectionChange(this, snapshot) }
106
126
  customSelectionActionModeCallback =
107
127
  createSelectionActionModeCallback(
108
128
  this,
@@ -113,6 +133,10 @@ class EnrichedMarkdownText
113
133
  onCustomItemPress = { itemText, selectedText, start, end ->
114
134
  onContextMenuItemPressCallback?.invoke(itemText, selectedText, start, end)
115
135
  },
136
+ // A full selection already carries the original markdown.
137
+ onCopyRequest = { markdown, text, fullSelection ->
138
+ onCopyRequestCallback?.invoke(markdown, text, fullSelection && currentMarkdown.isNotEmpty())
139
+ },
116
140
  )
117
141
  }
118
142
 
@@ -305,6 +329,93 @@ class EnrichedMarkdownText
305
329
  }
306
330
 
307
331
  applySelectionColors(selectionColor, selectionHandleColor)
332
+ emitLinkLayoutIfNeeded()
333
+ // New text drops the selection; report it if setText didn't already.
334
+ selectionReporter?.schedule()
335
+ }
336
+
337
+ fun setEmitSelectionChange(enabled: Boolean) {
338
+ selectionReporter?.enabled = enabled
339
+ }
340
+
341
+ // See HeadlessActionMode: with the menu hidden, no toolbar window is created.
342
+ override fun startActionMode(
343
+ callback: ActionMode.Callback?,
344
+ type: Int,
345
+ ): ActionMode? {
346
+ if (contextMenuHidden && type == ActionMode.TYPE_FLOATING && callback != null) {
347
+ return HeadlessActionMode.start(this, callback)
348
+ }
349
+ return super.startActionMode(callback, type)
350
+ }
351
+
352
+ // See isRomSelectionPopupShower: MIUI shows its own selection popup this way.
353
+ override fun postDelayed(
354
+ action: Runnable?,
355
+ delayMillis: Long,
356
+ ): Boolean {
357
+ if (contextMenuHidden && isRomSelectionPopupShower(action)) return false
358
+ return super.postDelayed(action, delayMillis)
359
+ }
360
+
361
+ /** Clears the selection (ending its ActionMode) and reports `active: false`. */
362
+ fun clearSelection() {
363
+ clearTextSelection()
364
+ activeActionMode?.finish()
365
+ selectionReporter?.flushNow()
366
+ }
367
+
368
+ override fun onSelectionChanged(
369
+ selStart: Int,
370
+ selEnd: Int,
371
+ ) {
372
+ super.onSelectionChanged(selStart, selEnd)
373
+ selectionReporter?.onSelectionChanged(selEnd > selStart)
374
+ }
375
+
376
+ private fun selectionSnapshot(): SelectionSnapshot {
377
+ val selection = currentTextSelection() ?: return SelectionSnapshot.INACTIVE
378
+ return SelectionSnapshot(
379
+ active = true,
380
+ source = "selection",
381
+ text = selection.text,
382
+ markdown = selection.markdown,
383
+ // Same rule as onCopyRequest: a full selection already carries the original markdown.
384
+ whole = selection.fullSelection && currentMarkdown.isNotEmpty(),
385
+ rect = selection.rect,
386
+ startRect = selection.startRect,
387
+ endRect = selection.endRect,
388
+ )
389
+ }
390
+
391
+ fun setEmitLinkLayout(enabled: Boolean) {
392
+ if (linkLayoutEnabled == enabled) return
393
+ linkLayoutEnabled = enabled
394
+ lastLinkLayout = null
395
+ emitLinkLayoutIfNeeded()
396
+ }
397
+
398
+ /**
399
+ * Reports link rects to JS (for `renderLinkDecoration`) after every render and layout;
400
+ * deduplicated so an unchanged layout — the common case while streaming — sends nothing.
401
+ */
402
+ private fun emitLinkLayoutIfNeeded() {
403
+ if (!linkLayoutEnabled || layout == null || width <= 0) return
404
+ val (fragments, _) = LinkLayoutCollector.collect(this)
405
+ if (fragments == lastLinkLayout) return
406
+ lastLinkLayout = fragments
407
+ emitLinkLayout(this, fragments)
408
+ }
409
+
410
+ override fun onLayout(
411
+ changed: Boolean,
412
+ left: Int,
413
+ top: Int,
414
+ right: Int,
415
+ bottom: Int,
416
+ ) {
417
+ super.onLayout(changed, left, top, right, bottom)
418
+ emitLinkLayoutIfNeeded()
308
419
  }
309
420
 
310
421
  // Trailing bottom margin included in the shadow-node measurement — must be
@@ -382,6 +493,7 @@ class EnrichedMarkdownText
382
493
  }
383
494
 
384
495
  override fun onDetachedFromWindow() {
496
+ selectionReporter?.cancel()
385
497
  stopSpoilerAnimations()
386
498
  super.onDetachedFromWindow()
387
499
  }
@@ -15,6 +15,7 @@ import com.facebook.react.viewmanagers.EnrichedMarkdownTextManagerInterface
15
15
  import com.facebook.yoga.YogaMeasureMode
16
16
  import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay
17
17
  import com.swmansion.enriched.markdown.utils.common.emitContextMenuItemPress
18
+ import com.swmansion.enriched.markdown.utils.common.emitCopyRequest
18
19
  import com.swmansion.enriched.markdown.utils.common.emitLinkLongPress
19
20
  import com.swmansion.enriched.markdown.utils.common.emitLinkPress
20
21
  import com.swmansion.enriched.markdown.utils.common.emitTaskListItemPress
@@ -42,6 +43,9 @@ class EnrichedMarkdownTextManager :
42
43
  view.onContextMenuItemPressCallback = { itemText, selectedText, selectionStart, selectionEnd ->
43
44
  emitContextMenuItemPress(view, itemText, selectedText, selectionStart, selectionEnd)
44
45
  }
46
+ view.onCopyRequestCallback = { markdown, text, whole ->
47
+ emitCopyRequest(view, "selection", markdown, text, whole)
48
+ }
45
49
 
46
50
  view.setOnLinkPressCallback { url ->
47
51
  emitLinkPress(view, url)
@@ -173,6 +177,28 @@ class EnrichedMarkdownTextManager :
173
177
  view?.setContextMenuHidden(contextMenuHidden)
174
178
  }
175
179
 
180
+ @ReactProp(name = "emitLinkLayout", defaultBoolean = false)
181
+ override fun setEmitLinkLayout(
182
+ view: EnrichedMarkdownText?,
183
+ emitLinkLayout: Boolean,
184
+ ) {
185
+ view?.setEmitLinkLayout(emitLinkLayout)
186
+ }
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
+
176
202
  @ReactProp(name = "enableLinkPreview", defaultBoolean = true)
177
203
  override fun setEnableLinkPreview(
178
204
  view: EnrichedMarkdownText?,
@@ -181,6 +207,14 @@ class EnrichedMarkdownTextManager :
181
207
  // No-op on Android — only used on iOS
182
208
  }
183
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
+
184
218
  @ReactProp(name = "enableTaskListItemToggle", defaultBoolean = true)
185
219
  override fun setEnableTaskListItemToggle(
186
220
  view: EnrichedMarkdownText?,