diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichSpan.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichSpan.kt index 06135f7e..10f55185 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichSpan.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichSpan.kt @@ -14,8 +14,7 @@ import kotlin.collections.indices /** * A rich span is a part of a rich paragraph. */ -@OptIn(ExperimentalRichTextApi::class) -internal class RichSpan( +class RichSpan( internal val key: Int? = null, val children: MutableList = mutableListOf(), var paragraph: RichParagraph, diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt index 5dc5e0f4..a97d9673 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt @@ -76,7 +76,7 @@ public class RichTextState internal constructor( internal var singleParagraphMode by mutableStateOf(false) - internal var textLayoutResult: TextLayoutResult? by mutableStateOf(null) + var textLayoutResult: TextLayoutResult? by mutableStateOf(null) private set private var lastPressPosition: Offset? by mutableStateOf(null) @@ -2722,7 +2722,7 @@ public class RichTextState internal constructor( return richSpan } - private fun getRichSpanByOffset(offset: Offset): RichSpan? { + fun getRichSpanByOffset(offset: Offset): RichSpan? { this.textLayoutResult?.let { textLayoutResult -> val position = textLayoutResult.getOffsetForPosition(offset) return getRichSpanByTextIndex(position, true) diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/RichParagraph.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/RichParagraph.kt index 997058d5..1022bf44 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/RichParagraph.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/RichParagraph.kt @@ -12,7 +12,7 @@ import com.mohamedrejeb.richeditor.paragraph.type.ParagraphType import com.mohamedrejeb.richeditor.paragraph.type.ParagraphType.Companion.startText import com.mohamedrejeb.richeditor.ui.test.getRichTextStyleTreeRepresentation -internal class RichParagraph( +class RichParagraph( val key: Int = 0, val children: MutableList = mutableListOf(), var paragraphStyle: ParagraphStyle = DefaultParagraphStyle, diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt index 7e40deba..c487ac33 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.text.ParagraphStyle import com.mohamedrejeb.richeditor.model.RichSpan import com.mohamedrejeb.richeditor.model.RichTextConfig -internal interface ParagraphType { +interface ParagraphType { fun getStyle(config: RichTextConfig): ParagraphStyle diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichTextEditor.kt index ec3e4574..f96af7a1 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/BasicRichTextEditor.kt @@ -19,15 +19,16 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +import com.mohamedrejeb.richeditor.model.RichSpan import com.mohamedrejeb.richeditor.model.RichTextState import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** @@ -65,6 +66,7 @@ import kotlinx.coroutines.CoroutineScope * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param maxLength the maximum length of the text field. If the text is longer than this value, * it will be ignored. The default value of this parameter is [Int.MAX_VALUE]. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or @@ -96,6 +98,7 @@ public fun BasicRichTextEditor( maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, + onRichSpanClick: RichSpanClickListener? = null, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, cursorBrush: Brush = SolidColor(Color.Black), @@ -114,6 +117,7 @@ public fun BasicRichTextEditor( maxLines = maxLines, minLines = minLines, maxLength = maxLength, + onRichSpanClick = onRichSpanClick, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = cursorBrush, @@ -157,6 +161,7 @@ public fun BasicRichTextEditor( * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param maxLength the maximum length of the text field. If the text is longer than this value, * it will be ignored. The default value of this parameter is [Int.MAX_VALUE]. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or @@ -189,6 +194,7 @@ public fun BasicRichTextEditor( maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, + onRichSpanClick: RichSpanClickListener? = null, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, cursorBrush: Brush = SolidColor(Color.Black), @@ -197,7 +203,6 @@ public fun BasicRichTextEditor( contentPadding: PaddingValues ) { val scope = rememberCoroutineScope() - val uriHandler = LocalUriHandler.current val density = LocalDensity.current val localTextStyle = LocalTextStyle.current val layoutDirection = LocalLayoutDirection.current @@ -213,13 +218,15 @@ public fun BasicRichTextEditor( state.singleParagraphMode = singleParagraph } - LaunchedEffect(interactionSource) { - scope.launch { - interactionSource.interactions.collect { interaction -> - if (interaction is PressInteraction.Press) { - val clickedLink = state.getLinkByOffset(interaction.pressPosition) - if (clickedLink != null) { - uriHandler.openUri(clickedLink) + if(onRichSpanClick != null) { + // Start listening for rich span clicks + LaunchedEffect(interactionSource) { + scope.launch { + interactionSource.interactions.collect { interaction -> + if (interaction is PressInteraction.Press) { + state.getRichSpanByOffset(interaction.pressPosition)?.let { clickedSpan -> + onRichSpanClick(clickedSpan) + } } } } @@ -328,4 +335,6 @@ internal suspend fun adjustTextIndicatorOffset( y = pressPosition.y - topPadding ), ) -} \ No newline at end of file +} + +typealias RichSpanClickListener = (RichSpan) -> Unit \ No newline at end of file diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditor.kt index 9c35d1b2..ca184552 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditor.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener /** * Material Design outlined rich text field @@ -65,6 +66,7 @@ import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param interactionSource the [MutableInteractionSource] representing the stream of * [Interaction]s for this OutlinedTextField. You can create and pass in your own remembered * [MutableInteractionSource] if you want to observe [Interaction]s and customize the @@ -92,6 +94,7 @@ public fun OutlinedRichTextEditor( maxLines: Int = Int.MAX_VALUE, minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, + onRichSpanClick: RichSpanClickListener? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = MaterialTheme.shapes.small, colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() @@ -130,6 +133,7 @@ public fun OutlinedRichTextEditor( maxLines = maxLines, minLines = minLines, maxLength = maxLength, + onRichSpanClick = onRichSpanClick, decorationBox = @Composable { innerTextField -> TextFieldDefaults.OutlinedTextFieldDecorationBox( value = state.textFieldValue.text, diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/RichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/RichTextEditor.kt index f480d9bb..c05346f0 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/RichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material/RichTextEditor.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor /** @@ -64,6 +65,7 @@ import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param interactionSource the [MutableInteractionSource] representing the stream of * [Interaction]s for this TextField. You can create and pass in your own remembered * [MutableInteractionSource] if you want to observe [Interaction]s and customize the @@ -98,6 +100,7 @@ public fun RichTextEditor( maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, + onRichSpanClick: RichSpanClickListener? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize), @@ -130,6 +133,7 @@ public fun RichTextEditor( maxLines = maxLines, minLines = minLines, maxLength = maxLength, + onRichSpanClick = onRichSpanClick, decorationBox = @Composable { innerTextField -> // places leading icon, text field with label and placeholder, trailing icon TextFieldDefaults.TextFieldDecorationBox( diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditor.kt index 4ca0f86f..ec5641d3 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditor.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.* import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener import kotlin.math.max import kotlin.math.roundToInt @@ -77,6 +78,7 @@ import kotlin.math.roundToInt * @param maxLength the maximum length of the text field. If the text is longer than this value, * it will be ignored. The default value of this parameter is [Int.MAX_VALUE]. * onTextLayout + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this text field. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this text field in different states. @@ -105,6 +107,7 @@ public fun OutlinedRichTextEditor( maxLines: Int = Int.MAX_VALUE, minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, + onRichSpanClick: RichSpanClickListener? = null, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RichTextEditorDefaults.outlinedShape, @@ -145,6 +148,7 @@ public fun OutlinedRichTextEditor( maxLines = maxLines, minLines = minLines, maxLength = maxLength, + onRichSpanClick = onRichSpanClick, onTextLayout = onTextLayout, decorationBox = { innerTextField -> RichTextEditorDefaults.OutlinedRichTextEditorDecorationBox( diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/RichTextEditor.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/RichTextEditor.kt index f9ae4908..819ab1b2 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/RichTextEditor.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/ui/material3/RichTextEditor.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.* -import androidx.compose.material3.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember @@ -21,7 +20,6 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.* -import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction @@ -29,6 +27,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.* import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.ui.BasicRichTextEditor +import com.mohamedrejeb.richeditor.ui.RichSpanClickListener import kotlin.math.max import kotlin.math.roundToInt @@ -76,6 +75,7 @@ import kotlin.math.roundToInt * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param maxLength the maximum length of the text field. If the text is longer than this value, * it will be ignored. The default value of this parameter is [Int.MAX_VALUE]. + * @param onRichSpanClick A callback to allow handling of click on RichSpans. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this text field. You can create and pass in your own `remember`ed instance to observe * [Interaction]s and customize the appearance / behavior of this text field in different states. @@ -104,6 +104,7 @@ public fun RichTextEditor( maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, maxLength: Int = Int.MAX_VALUE, + onRichSpanClick: RichSpanClickListener? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RichTextEditorDefaults.filledShape, colors: RichTextEditorColors = RichTextEditorDefaults.richTextEditorColors(), @@ -137,6 +138,7 @@ public fun RichTextEditor( maxLines = maxLines, minLines = minLines, maxLength = maxLength, + onRichSpanClick = onRichSpanClick, interactionSource = interactionSource, cursorBrush = SolidColor(colors.cursorColor(isError).value), decorationBox = @Composable { innerTextField ->