Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- 🔀 [Markdown Streaming](docs/MARKDOWN_STREAMING.md) support (via [react-native-streamdown](https://github.com/software-mansion-labs/react-native-streamdown))
- 🎨 Fully customizable styles for all elements
- ✨ Text selection and copy support
- 📌 Custom text selection context menu items
- 🔗 Interactive link handling
- 🖼️ Native image interactions (iOS: Copy, Save to Camera Roll)
- 🌐 Native platform features (Translate, Look Up, Search Web, Share)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class EnrichedMarkdown
private var onLinkPressCallback: ((String) -> Unit)? = null
private var onLinkLongPressCallback: ((String) -> Unit)? = null
private var onTaskListItemPressCallback: ((Int, Boolean, String) -> Unit)? = null
private var contextMenuItemTexts: List<String> = emptyList()
var onContextMenuItemPressCallback: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null

fun setMarkdownContent(markdown: String) {
if (currentMarkdown == markdown) return
Expand Down Expand Up @@ -147,6 +149,22 @@ class EnrichedMarkdown
onTaskListItemPressCallback = callback
}

fun setContextMenuItems(items: List<String>) {
contextMenuItemTexts = items
segmentViews.filterIsInstance<EnrichedMarkdownInternalText>().forEach {
it.setContextMenuItems(items, ::forwardContextMenuItemPress)
}
}

private fun forwardContextMenuItemPress(
itemText: String,
selectedText: String,
selectionStart: Int,
selectionEnd: Int,
) {
onContextMenuItemPressCallback?.invoke(itemText, selectedText, selectionStart, selectionEnd)
}

private fun recreateStyleConfig() {
markdownStyleMap?.let {
markdownStyle = StyleConfig(it, context, allowFontScaling, maxFontSizeMultiplier)
Expand Down Expand Up @@ -261,6 +279,10 @@ class EnrichedMarkdown
onTaskListItemPressCallback = { taskIndex, checked, itemText ->
this@EnrichedMarkdown.onTaskListItemPressCallback?.invoke(taskIndex, checked, itemText)
}

if (contextMenuItemTexts.isNotEmpty()) {
setContextMenuItems(contextMenuItemTexts, ::forwardContextMenuItemPress)
}
}

private fun createTableView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMeth
import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback
import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
import com.swmansion.enriched.markdown.views.BlockSegmentView

Expand All @@ -36,8 +37,19 @@ class EnrichedMarkdownInternalText

override val segmentMarginBottom: Int get() = lastElementMarginBottom.toInt()

private var contextMenuItemTexts: List<String> = emptyList()
private var onContextMenuItemPress: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null

init {
setupAsMarkdownTextView(accessibilityHelper)
customSelectionActionModeCallback =
createSelectionActionModeCallback(
this,
getCustomItemTexts = { contextMenuItemTexts },
onCustomItemPress = { itemText, selectedText, start, end ->
onContextMenuItemPress?.invoke(itemText, selectedText, start, end)
},
)
}

fun applyStyledText(styledText: CharSequence) {
Expand All @@ -54,6 +66,14 @@ class EnrichedMarkdownInternalText
applySelectableState(selectable)
}

fun setContextMenuItems(
items: List<String>,
onPress: (itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit,
) {
contextMenuItemTexts = items
onContextMenuItemPress = onPress
}

override fun onTouchEvent(event: MotionEvent): Boolean {
if (checkboxTouchHelper.onTouchEvent(event)) {
if (event.action == MotionEvent.ACTION_DOWN) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swmansion.enriched.markdown

import android.content.Context
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
Expand All @@ -11,6 +12,7 @@ import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.EnrichedMarkdownManagerDelegate
import com.facebook.react.viewmanagers.EnrichedMarkdownManagerInterface
import com.facebook.yoga.YogaMeasureMode
import com.swmansion.enriched.markdown.events.ContextMenuItemPressEvent
import com.swmansion.enriched.markdown.events.LinkLongPressEvent
import com.swmansion.enriched.markdown.events.LinkPressEvent
import com.swmansion.enriched.markdown.events.TaskListItemPressEvent
Expand All @@ -28,14 +30,22 @@ class EnrichedMarkdownManager :

override fun getName(): String = NAME

override fun createViewInstance(reactContext: ThemedReactContext): EnrichedMarkdown = EnrichedMarkdown(reactContext)
override fun createViewInstance(reactContext: ThemedReactContext): EnrichedMarkdown {
val view = EnrichedMarkdown(reactContext)
view.onContextMenuItemPressCallback = { itemText, selectedText, selectionStart, selectionEnd ->
emitContextMenuItemPress(view, itemText, selectedText, selectionStart, selectionEnd)
}
return view
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
val map = mutableMapOf<String, Any>()
map[LinkPressEvent.EVENT_NAME] = mapOf("registrationName" to LinkPressEvent.EVENT_NAME)
map[LinkLongPressEvent.EVENT_NAME] = mapOf("registrationName" to LinkLongPressEvent.EVENT_NAME)
map[TaskListItemPressEvent.EVENT_NAME] =
mapOf("registrationName" to TaskListItemPressEvent.EVENT_NAME)
map[ContextMenuItemPressEvent.EVENT_NAME] =
mapOf("registrationName" to ContextMenuItemPressEvent.EVENT_NAME)
return map
}

Expand Down Expand Up @@ -132,6 +142,16 @@ class EnrichedMarkdownManager :
// Currently only supported with flavor="commonmark" (single TextView).
}

@ReactProp(name = "contextMenuItems")
override fun setContextMenuItems(
view: EnrichedMarkdown?,
value: ReadableArray?,
) {
if (view == null) return
val items = (0 until (value?.size() ?: 0)).mapNotNull { value?.getMap(it)?.getString("text") }
view.setContextMenuItems(items)
}

override fun setPadding(
view: EnrichedMarkdown,
left: Int,
Expand All @@ -143,6 +163,19 @@ class EnrichedMarkdownManager :
view.setPadding(left, top, right, bottom)
}

private fun emitContextMenuItemPress(
view: EnrichedMarkdown,
itemText: String,
selectedText: String,
selectionStart: Int,
selectionEnd: Int,
) {
val context = view.context as com.facebook.react.bridge.ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(context)
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
eventDispatcher?.dispatchEvent(ContextMenuItemPressEvent(surfaceId, view.id, itemText, selectedText, selectionStart, selectionEnd))
}

private fun emitOnLinkPress(
view: EnrichedMarkdown,
url: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMeth
import com.swmansion.enriched.markdown.utils.text.view.applySelectableState
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForCheckboxTap
import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
import com.swmansion.enriched.markdown.utils.text.view.createSelectionActionModeCallback
import com.swmansion.enriched.markdown.utils.text.view.emitLinkLongPressEvent
import com.swmansion.enriched.markdown.utils.text.view.emitLinkPressEvent
import com.swmansion.enriched.markdown.utils.text.view.setupAsMarkdownTextView
Expand Down Expand Up @@ -53,6 +54,9 @@ class EnrichedMarkdownText
// Accessibility helper for TalkBack support
private val accessibilityHelper = MarkdownAccessibilityHelper(this)

private var contextMenuItemTexts: List<String> = emptyList()
var onContextMenuItemPressCallback: ((itemText: String, selectedText: String, selectionStart: Int, selectionEnd: Int) -> Unit)? = null

var markdownStyle: StyleConfig? = null
private set

Expand All @@ -75,6 +79,14 @@ class EnrichedMarkdownText

init {
setupAsMarkdownTextView(accessibilityHelper)
customSelectionActionModeCallback =
createSelectionActionModeCallback(
this,
getCustomItemTexts = { contextMenuItemTexts },
onCustomItemPress = { itemText, selectedText, start, end ->
onContextMenuItemPressCallback?.invoke(itemText, selectedText, start, end)
},
)
}

fun setMarkdownContent(markdown: String) {
Expand Down Expand Up @@ -232,6 +244,10 @@ class EnrichedMarkdownText
}
}

fun setContextMenuItems(items: List<String>) {
contextMenuItemTexts = items
}

fun setIsSelectable(selectable: Boolean) {
applySelectableState(selectable)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swmansion.enriched.markdown

import android.content.Context
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
Expand All @@ -11,6 +12,7 @@ import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.EnrichedMarkdownTextManagerDelegate
import com.facebook.react.viewmanagers.EnrichedMarkdownTextManagerInterface
import com.facebook.yoga.YogaMeasureMode
import com.swmansion.enriched.markdown.events.ContextMenuItemPressEvent
import com.swmansion.enriched.markdown.events.LinkLongPressEvent
import com.swmansion.enriched.markdown.events.LinkPressEvent
import com.swmansion.enriched.markdown.events.TaskListItemPressEvent
Expand All @@ -29,7 +31,13 @@ class EnrichedMarkdownTextManager :

override fun getName(): String = NAME

override fun createViewInstance(reactContext: ThemedReactContext): EnrichedMarkdownText = EnrichedMarkdownText(reactContext)
override fun createViewInstance(reactContext: ThemedReactContext): EnrichedMarkdownText {
val view = EnrichedMarkdownText(reactContext)
view.onContextMenuItemPressCallback = { itemText, selectedText, selectionStart, selectionEnd ->
emitContextMenuItemPress(view, itemText, selectedText, selectionStart, selectionEnd)
}
return view
}

override fun onDropViewInstance(view: EnrichedMarkdownText) {
super.onDropViewInstance(view)
Expand All @@ -44,6 +52,8 @@ class EnrichedMarkdownTextManager :
map[LinkLongPressEvent.EVENT_NAME] = mapOf("registrationName" to LinkLongPressEvent.EVENT_NAME)
map[TaskListItemPressEvent.EVENT_NAME] =
mapOf("registrationName" to TaskListItemPressEvent.EVENT_NAME)
map[ContextMenuItemPressEvent.EVENT_NAME] =
mapOf("registrationName" to ContextMenuItemPressEvent.EVENT_NAME)
return map
}

Expand Down Expand Up @@ -152,6 +162,16 @@ class EnrichedMarkdownTextManager :
view?.setStreamingAnimation(streamingAnimation)
}

@ReactProp(name = "contextMenuItems")
override fun setContextMenuItems(
view: EnrichedMarkdownText?,
value: ReadableArray?,
) {
if (view == null) return
val items = (0 until (value?.size() ?: 0)).mapNotNull { value?.getMap(it)?.getString("text") }
view.setContextMenuItems(items)
}

override fun setPadding(
view: EnrichedMarkdownText,
left: Int,
Expand Down Expand Up @@ -187,6 +207,19 @@ class EnrichedMarkdownTextManager :
eventDispatcher?.dispatchEvent(event)
}

private fun emitContextMenuItemPress(
view: EnrichedMarkdownText,
itemText: String,
selectedText: String,
selectionStart: Int,
selectionEnd: Int,
) {
val context = view.context as com.facebook.react.bridge.ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(context)
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
eventDispatcher?.dispatchEvent(ContextMenuItemPressEvent(surfaceId, view.id, itemText, selectedText, selectionStart, selectionEnd))
}

private fun emitOnTaskListItemPress(
view: EnrichedMarkdownText,
taskIndex: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.swmansion.enriched.markdown.events

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class ContextMenuItemPressEvent(
surfaceId: Int,
viewId: Int,
private val itemText: String,
private val selectedText: String,
private val selectionStart: Int,
private val selectionEnd: Int,
) : Event<ContextMenuItemPressEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME

override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putString("itemText", itemText)
putString("selectedText", selectedText)
putInt("selectionStart", selectionStart)
putInt("selectionEnd", selectionEnd)
}

companion object {
const val EVENT_NAME = "onContextMenuItemPress"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.swmansion.enriched.markdown.input

import android.content.Context
import android.graphics.Color
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ReactStylesDiffMap
Expand All @@ -17,6 +18,7 @@ import com.swmansion.enriched.markdown.input.events.OnChangeMarkdownEvent
import com.swmansion.enriched.markdown.input.events.OnChangeSelectionEvent
import com.swmansion.enriched.markdown.input.events.OnChangeStateEvent
import com.swmansion.enriched.markdown.input.events.OnChangeTextEvent
import com.swmansion.enriched.markdown.input.events.OnContextMenuItemPressEvent
import com.swmansion.enriched.markdown.input.events.OnInputBlurEvent
import com.swmansion.enriched.markdown.input.events.OnInputFocusEvent
import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent
Expand Down Expand Up @@ -81,6 +83,7 @@ class EnrichedMarkdownInputManager :
OnRequestMarkdownResultEvent.EVENT_NAME,
OnInputFocusEvent.EVENT_NAME,
OnInputBlurEvent.EVENT_NAME,
OnContextMenuItemPressEvent.EVENT_NAME,
).associateWithTo(mutableMapOf()) { name -> mapOf("registrationName" to name) }

// Props
Expand Down Expand Up @@ -234,6 +237,16 @@ class EnrichedMarkdownInputManager :
view?.emitMarkdown = value
}

@ReactProp(name = "contextMenuItems")
override fun setContextMenuItems(
view: EnrichedMarkdownInputView?,
value: ReadableArray?,
) {
if (view == null) return
val items = (0 until (value?.size() ?: 0)).mapNotNull { value?.getMap(it)?.getString("text") }
view.setContextMenuItems(items)
}

override fun updateProperties(
view: EnrichedMarkdownInputView,
props: ReactStylesDiffMap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ class EnrichedMarkdownInputView(
applyFormattingAndEmit()
}

fun setContextMenuItems(items: List<String>) {
contextMenu.setContextMenuItems(items)
}

fun setValueFromJS(markdown: String) {
val parsed = InputParser.parseToPlainTextAndRanges(markdown)
blockEmitting = true
Expand Down
Loading
Loading