Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Wavesonics committed Jun 22, 2022
0 parents commit 56fe91c
Show file tree
Hide file tree
Showing 69 changed files with 2,985 additions and 0 deletions.
42 changes: 42 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/

### Mac OS ###
.DS_Store
241 changes: 241 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Jetpack Compose Rich Text Editor

[![](https://jitpack.io/v/pChochura/richtext-compose.svg)](https://jitpack.io/#pChochura/richtext-compose)

# This is a hard fork of the fanstastic library of the same name from pChochura. This fork makes it into a multi-platform library

I've been looking for a library that is able to deliver an editable component which can render rich
text in real time. The main issue with libraries I found was that they were using WebView and
Javascript under the hood. I wanted something compatible with Jetpack Compose.

So the only solution was to create my own library.

# Installation

1. Add a link to the Jitpack repository

```groovy
repositories {
maven { url 'https://jitpack.io' }
}
```

2. Include link do the library (change the version to the current one)

```groovy
implementation "com.github.pChochura:richtext-compose:$version"
```

# Usage

Insert function call to a Composable:

```kotlin
var value by remember { mutableStateOf(RichTextValue.get()) }

RichTextEditor(
modifier = Modifier,
value = value,
onValueChange = { value = it },
textFieldStyle = defaultRichTextFieldStyle().copy(
placeholder = "My rich text editor in action",
textColor = MaterialTheme.colors.onPrimary,
placeholderColor = MaterialTheme.colors.secondaryVariant,
)
)

// If you want to render static text use `RichText` instead
RichText(
modifier = Modifier,
value = value,
textStyle = defaultRichTextStyle().copy(
textColor = MaterialTheme.colors.onPrimary,
)
)
```

You can easily stylize the TextField that is under the hood by providing values for
the `textFieldStyle`. Default ones are as follows:

```kotlin
@Composable
fun defaultRichTextFieldStyle() = RichTextFieldStyle(
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
),
placeholder = EMPTY_STRING,
textStyle = MaterialTheme.typography.body1,
textColor = MaterialTheme.colors.onPrimary,
placeholderColor = MaterialTheme.colors.secondaryVariant,
cursorColor = MaterialTheme.colors.secondary,
)
```

To insert or clear styles you can use methods provided by the `RichTextValue` object:

```kotlin
abstract class RichTextValue {
/**
* Returns styles that are used inside the current selection (or composition)
*/
abstract val currentStyles: Set<Style>
abstract val isUndoAvailable: Boolean
abstract val isRedoAvailable: Boolean

abstract fun insertStyle(style: Style): RichTextValue
abstract fun clearStyles(vararg styles: Style): RichTextValue

abstract fun undo(): RichTextValue
abstract fun redo(): RichTextValue
}
```

Every method that manipulates the text inside the TextField returns a copy of the object to be
passed to a state.

## Available styles

1. Bold
```kotlin
// Inserting style
value = value.insertStyle(Style.Bold)

// Checking if the style is used inside the current selection
val isInCurrentSelection = value.currentStyles.contains(Style.Bold)
```

2. Underline
```kotlin
// Inserting style
value = value.insertStyle(Style.Underline)

// Checking if the style is used inside the current selection
val isInCurrentSelection = value.currentStyles.contains(Style.Underline)
```

3. Italic
```kotlin
// Inserting style
value = value.insertStyle(Style.Italic)

// Checking if the style is used inside the current selection
val isInCurrentSelection = value.currentStyles.contains(Style.Italic)
```

4. Strikethrough
```kotlin
// Inserting style
value = value.insertStyle(Style.Strikethrough)

// Checking if the style is used inside the current selection
val isInCurrentSelection = value.currentStyles.contains(Style.Strikethrough)
```

5. Align Left
```kotlin
// Inserting style
value = value.insertStyle(Style.AlignLeft)

// Checking if the style is used inside the current selection
val isInCurrentSelection = value.currentStyles.contains(Style.AlignLeft)
```

6. Align Center
```kotlin
// Inserting style
value = value.insertStyle(Style.AlignCenter)

// Checking if the style is used inside the current selection
val isInCurrentSelection = value.currentStyles.contains(Style.AlignCenter)
```

7. Align Right
```kotlin
// Inserting style
value = value.insertStyle(Style.AlignRight)

// Checking if the style is used inside the current selection
val isInCurrentSelection = value.currentStyles.contains(Style.AlignRight)
```

8. Text Size
```kotlin
// We're deleting all of the text size styles from the selection to avoid multiple multiplications of the size
value = value.clearStyles(Style.TextColor())

// Inserting style
// You have to pass size as a parameter. It accepts values between 0.5f and 2.0f
// Which means that the text size will be multiplied by the provided value
value = value.insertStyle(Style.TextSize(textSize))

// Checking if the style is used inside the current selection
// Here we're using `filterIsInstance` to check if there are any of the text size styles
val isInCurrentSelection = value.currentStyles.filterIsInstance<Style.TextSize>().isNotEmpty()
```

9. Text Color
```kotlin
// We're deleting all of the text color styles from the selection to avoid having more than one color on the same portion of the text (the last one would be displayed either way)
value = value.clearStyles(Style.TextColor())

// Inserting style
// You have to pass color as a parameter
value = value.insertStyle(Style.TextColor(color))

// Checking if the style is used inside the current selection
// Here we're using `filterIsInstance` to check if there are any of the text color styles
val isInCurrentSelection = value.value.currentStyles.filterIsInstance<Style.TextColor>().isNotEmpty()
```

## Custom styling

If you want to create your own styles you're free to do so. You would have to create a class that
extends `StyleMapper` and implement the styling there for the styles that you would have created.

```kotlin
// If you want to create a paragraph style you have to extend `ParagraphStyle` interface!
object CustomParagraphStyle : Style
object CustomStyle : Style

class CustomStyleMapper : StyleMapper() {

override fun fromTag(tag: String): Style =
runCatching { super.fromTag(tag) }.getOrNull() ?: when (tag) {
// It is necessary to ensure undo/redo actions work correctly
"${CustomStyle.javaClass.simpleName}/" -> CustomStyle
"${CustomParagraphStyle.javaClass.simpleName}/" -> CustomParagraphStyle
else -> throw IllegalArgumentException()
}

override fun toSpanStyle(style: Style): SpanStyle? = super.toSpanStyle(style) ?: when (style) {
// Here we're customizing the behavior of the style
is CustomStyle -> SpanStyle(
color = Color.Red,
fontWeight = FontWeight.Bold,
)
else -> null
}

override fun toParagraphStyle(style: Style): ParagraphStyle? =
super.toParagraphStyle(style) ?: when (style) {
is CustomParagraphStyle -> ParagraphStyle(
textAlign = TextAlign.Justify,
textIndent = TextIndent(firstLine = 12.sp)
)
else -> null
}
}
```

And then you would have to pass an instance of the class you created as a parameter to
the `RichTextValue` class:

```kotlin
var value by remember {
mutableStateOf(
RichTextValue.get(
styleMapper = CustomStyleMapper()
)
)
}
```
54 changes: 54 additions & 0 deletions androidExample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
val compose_version: String by extra

plugins {
id("org.jetbrains.compose")
id("com.android.application")
kotlin("android")
}

group "com.darkrockstudios.example.richtexteditor"
version "1.0-SNAPSHOT"

repositories {

}

dependencies {
implementation(project(":library"))
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
implementation("androidx.activity:activity-compose:1.4.0")

implementation("androidx.compose.ui:ui:$compose_version")
implementation("androidx.compose.material:material:$compose_version")
implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
}

android {
compileSdk = 31
defaultConfig {
applicationId = "com.pointlessapps.example"
minSdk = 24
targetSdk = 31
versionCode = 1
versionName = "1.0-SNAPSHOT"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
File("proguard-rules.pro")
)
}
}
packagingOptions {
resources {
excludes += mutableSetOf("/META-INF/{AL2.0,LGPL2.1}")
}
}
}
14 changes: 14 additions & 0 deletions androidExample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.darkrockstudios.example">
<application
android:allowBackup="false"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity android:name="com.darkrockstudios.example.MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Loading

0 comments on commit 56fe91c

Please sign in to comment.