@chaitrabhairappa/react-native-rich-text-editor 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/richtext/editor/FloatingToolbar.kt +350 -0
- package/android/src/main/java/com/richtext/editor/RichTextEditorPackage.kt +16 -0
- package/android/src/main/java/com/richtext/editor/RichTextEditorView.kt +1292 -0
- package/android/src/main/java/com/richtext/editor/RichTextEditorViewManager.kt +236 -0
- package/ios/RichTextEditorView.swift +1574 -0
- package/ios/RichTextEditorViewManager.m +45 -0
- package/ios/RichTextEditorViewManager.swift +235 -0
- package/lib/commonjs/index.js +156 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +8 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/index.js +143 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/src/index.d.ts +7 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +76 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +78 -0
- package/react-native-richtext-editor.podspec +21 -0
- package/src/index.tsx +199 -0
- package/src/types.ts +125 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Chaitra Bhairappa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# react-native-richtext-editor
|
|
2
|
+
|
|
3
|
+
A powerful native rich text editor for React Native with support for text formatting, lists, and more. Works on both iOS and Android.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Bold, Italic, Underline, Strikethrough
|
|
8
|
+
- Code and Highlight formatting
|
|
9
|
+
- Bullet lists and Numbered lists
|
|
10
|
+
- Headings
|
|
11
|
+
- Quotes and Checklists
|
|
12
|
+
- Link insertion
|
|
13
|
+
- Undo/Redo
|
|
14
|
+
- Text alignment (left, center, right)
|
|
15
|
+
- Indent/Outdent
|
|
16
|
+
- Floating toolbar with customizable options
|
|
17
|
+
- Two variants: outlined and flat
|
|
18
|
+
- Auto-growing height
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install react-native-richtext-editor
|
|
24
|
+
# or
|
|
25
|
+
yarn add react-native-richtext-editor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### iOS
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd ios && pod install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Android
|
|
35
|
+
|
|
36
|
+
No additional setup required.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import React, { useRef } from 'react';
|
|
42
|
+
import { View } from 'react-native';
|
|
43
|
+
import RichTextEditor, {
|
|
44
|
+
RichTextEditorRef,
|
|
45
|
+
Block,
|
|
46
|
+
ContentChangeEvent
|
|
47
|
+
} from 'react-native-richtext-editor';
|
|
48
|
+
|
|
49
|
+
const App = () => {
|
|
50
|
+
const editorRef = useRef<RichTextEditorRef>(null);
|
|
51
|
+
|
|
52
|
+
const handleContentChange = (event: ContentChangeEvent) => {
|
|
53
|
+
console.log('Content changed:', event.nativeEvent.blocks);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const initialContent: Block[] = [
|
|
57
|
+
{
|
|
58
|
+
type: 'paragraph',
|
|
59
|
+
text: 'Hello World',
|
|
60
|
+
styles: [{ style: 'bold', start: 0, end: 5 }],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'bullet',
|
|
64
|
+
text: 'First item',
|
|
65
|
+
styles: [],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'bullet',
|
|
69
|
+
text: 'Second item',
|
|
70
|
+
styles: [{ style: 'italic', start: 0, end: 6 }],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View style={{ flex: 1, padding: 16 }}>
|
|
76
|
+
<RichTextEditor
|
|
77
|
+
ref={editorRef}
|
|
78
|
+
placeholder="Enter text..."
|
|
79
|
+
initialContent={initialContent}
|
|
80
|
+
onContentChange={handleContentChange}
|
|
81
|
+
maxHeight={300}
|
|
82
|
+
variant="outlined"
|
|
83
|
+
/>
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default App;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Props
|
|
92
|
+
|
|
93
|
+
| Prop | Type | Default | Description |
|
|
94
|
+
|------|------|---------|-------------|
|
|
95
|
+
| `placeholder` | `string` | `""` | Placeholder text |
|
|
96
|
+
| `initialContent` | `Block[]` | `[]` | Initial content blocks |
|
|
97
|
+
| `readOnly` | `boolean` | `false` | Make editor read-only |
|
|
98
|
+
| `maxHeight` | `number` | `undefined` | Maximum height before scrolling |
|
|
99
|
+
| `showToolbar` | `boolean` | `true` | Show/hide floating toolbar |
|
|
100
|
+
| `toolbarOptions` | `ToolbarOption[]` | All options | Customize toolbar buttons |
|
|
101
|
+
| `variant` | `'outlined' \| 'flat'` | `'outlined'` | Editor style variant |
|
|
102
|
+
| `onContentChange` | `(event: ContentChangeEvent) => void` | `undefined` | Called when content changes |
|
|
103
|
+
| `onSelectionChange` | `(event: SelectionChangeEvent) => void` | `undefined` | Called when selection changes |
|
|
104
|
+
| `onFocus` | `() => void` | `undefined` | Called when editor gains focus |
|
|
105
|
+
| `onBlur` | `() => void` | `undefined` | Called when editor loses focus |
|
|
106
|
+
|
|
107
|
+
## Ref Methods
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
const editorRef = useRef<RichTextEditorRef>(null);
|
|
111
|
+
|
|
112
|
+
// Content management
|
|
113
|
+
editorRef.current?.setContent(blocks);
|
|
114
|
+
editorRef.current?.clear();
|
|
115
|
+
const text = await editorRef.current?.getText();
|
|
116
|
+
const blocks = await editorRef.current?.getBlocks();
|
|
117
|
+
|
|
118
|
+
// Focus management
|
|
119
|
+
editorRef.current?.focus();
|
|
120
|
+
editorRef.current?.blur();
|
|
121
|
+
|
|
122
|
+
// Text styles
|
|
123
|
+
editorRef.current?.toggleBold();
|
|
124
|
+
editorRef.current?.toggleItalic();
|
|
125
|
+
editorRef.current?.toggleUnderline();
|
|
126
|
+
editorRef.current?.toggleStrikethrough();
|
|
127
|
+
editorRef.current?.toggleCode();
|
|
128
|
+
editorRef.current?.toggleHighlight();
|
|
129
|
+
|
|
130
|
+
// Block types
|
|
131
|
+
editorRef.current?.setHeading();
|
|
132
|
+
editorRef.current?.setBulletList();
|
|
133
|
+
editorRef.current?.setNumberedList();
|
|
134
|
+
editorRef.current?.setQuote();
|
|
135
|
+
editorRef.current?.setChecklist();
|
|
136
|
+
editorRef.current?.setParagraph();
|
|
137
|
+
|
|
138
|
+
// Actions
|
|
139
|
+
editorRef.current?.insertLink(url, text);
|
|
140
|
+
editorRef.current?.undo();
|
|
141
|
+
editorRef.current?.redo();
|
|
142
|
+
editorRef.current?.clearFormatting();
|
|
143
|
+
|
|
144
|
+
// Indentation
|
|
145
|
+
editorRef.current?.indent();
|
|
146
|
+
editorRef.current?.outdent();
|
|
147
|
+
|
|
148
|
+
// Alignment
|
|
149
|
+
editorRef.current?.setAlignment('left' | 'center' | 'right');
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Types
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
interface Block {
|
|
156
|
+
type: BlockType;
|
|
157
|
+
text: string;
|
|
158
|
+
styles: StyleRange[];
|
|
159
|
+
alignment?: TextAlignment;
|
|
160
|
+
checked?: boolean;
|
|
161
|
+
indentLevel?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type BlockType = 'paragraph' | 'bullet' | 'numbered' | 'heading' | 'quote' | 'checklist';
|
|
165
|
+
type TextAlignment = 'left' | 'center' | 'right';
|
|
166
|
+
|
|
167
|
+
interface StyleRange {
|
|
168
|
+
style: 'bold' | 'italic' | 'underline' | 'strikethrough' | 'link' | 'code' | 'highlight';
|
|
169
|
+
start: number;
|
|
170
|
+
end: number;
|
|
171
|
+
url?: string;
|
|
172
|
+
highlightColor?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type ToolbarOption =
|
|
176
|
+
| 'bold' | 'italic' | 'strikethrough' | 'underline' | 'code' | 'highlight'
|
|
177
|
+
| 'heading' | 'bullet' | 'numbered' | 'quote' | 'checklist'
|
|
178
|
+
| 'link' | 'undo' | 'redo' | 'clearFormatting'
|
|
179
|
+
| 'indent' | 'outdent'
|
|
180
|
+
| 'alignLeft' | 'alignCenter' | 'alignRight';
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Customizing Toolbar
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import RichTextEditor, { ToolbarOption } from 'react-native-richtext-editor';
|
|
187
|
+
|
|
188
|
+
const toolbarOptions: ToolbarOption[] = [
|
|
189
|
+
'bold',
|
|
190
|
+
'italic',
|
|
191
|
+
'underline',
|
|
192
|
+
'bullet',
|
|
193
|
+
'numbered',
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
<RichTextEditor
|
|
197
|
+
toolbarOptions={toolbarOptions}
|
|
198
|
+
// ...
|
|
199
|
+
/>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Android Setup
|
|
203
|
+
|
|
204
|
+
Add the package to your `MainApplication.java` or `MainApplication.kt`:
|
|
205
|
+
|
|
206
|
+
```java
|
|
207
|
+
// MainApplication.java
|
|
208
|
+
import com.richtext.editor.RichTextEditorPackage;
|
|
209
|
+
|
|
210
|
+
@Override
|
|
211
|
+
protected List<ReactPackage> getPackages() {
|
|
212
|
+
List<ReactPackage> packages = new PackageList(this).getPackages();
|
|
213
|
+
packages.add(new RichTextEditorPackage());
|
|
214
|
+
return packages;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = {prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
mavenCentral()
|
|
8
|
+
}
|
|
9
|
+
dependencies {
|
|
10
|
+
classpath("com.android.tools.build:gradle:7.4.2")
|
|
11
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.8.0')}")
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def isNewArchitectureEnabled() {
|
|
16
|
+
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply plugin: 'com.android.library'
|
|
20
|
+
apply plugin: 'kotlin-android'
|
|
21
|
+
|
|
22
|
+
android {
|
|
23
|
+
namespace "com.richtext.editor"
|
|
24
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 34)
|
|
25
|
+
|
|
26
|
+
defaultConfig {
|
|
27
|
+
minSdkVersion safeExtGet('minSdkVersion', 21)
|
|
28
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 34)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
buildTypes {
|
|
32
|
+
release {
|
|
33
|
+
minifyEnabled false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
compileOptions {
|
|
38
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
39
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
kotlinOptions {
|
|
43
|
+
jvmTarget = '17'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
sourceSets {
|
|
47
|
+
main {
|
|
48
|
+
java.srcDirs = ['src/main/java']
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
repositories {
|
|
54
|
+
google()
|
|
55
|
+
mavenCentral()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
dependencies {
|
|
59
|
+
implementation "com.facebook.react:react-native:+"
|
|
60
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '1.8.0')}"
|
|
61
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
package com.richtext.editor
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.drawable.GradientDrawable
|
|
6
|
+
import android.view.Gravity
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.view.ViewGroup
|
|
9
|
+
import android.widget.HorizontalScrollView
|
|
10
|
+
import android.widget.LinearLayout
|
|
11
|
+
import android.widget.TextView
|
|
12
|
+
import android.graphics.Typeface
|
|
13
|
+
import android.text.SpannableString
|
|
14
|
+
import android.text.Spanned
|
|
15
|
+
import android.text.style.StrikethroughSpan
|
|
16
|
+
import android.text.style.UnderlineSpan
|
|
17
|
+
import android.text.style.BackgroundColorSpan
|
|
18
|
+
|
|
19
|
+
class FloatingToolbar(context: Context) : LinearLayout(context) {
|
|
20
|
+
|
|
21
|
+
interface ToolbarActionListener {
|
|
22
|
+
fun onBoldClick()
|
|
23
|
+
fun onItalicClick()
|
|
24
|
+
fun onUnderlineClick()
|
|
25
|
+
fun onStrikethroughClick()
|
|
26
|
+
fun onCodeClick()
|
|
27
|
+
fun onHighlightClick()
|
|
28
|
+
fun onHeadingClick()
|
|
29
|
+
fun onBulletListClick()
|
|
30
|
+
fun onNumberedListClick()
|
|
31
|
+
fun onQuoteClick()
|
|
32
|
+
fun onChecklistClick()
|
|
33
|
+
fun onLinkClick()
|
|
34
|
+
fun onUndoClick()
|
|
35
|
+
fun onRedoClick()
|
|
36
|
+
fun onClearFormattingClick()
|
|
37
|
+
fun onIndentClick()
|
|
38
|
+
fun onOutdentClick()
|
|
39
|
+
fun onAlignLeftClick()
|
|
40
|
+
fun onAlignCenterClick()
|
|
41
|
+
fun onAlignRightClick()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var listener: ToolbarActionListener? = null
|
|
45
|
+
|
|
46
|
+
private val density = context.resources.displayMetrics.density
|
|
47
|
+
private val buttonSize = (36 * density).toInt()
|
|
48
|
+
private val toolbarHeight = (52 * density).toInt()
|
|
49
|
+
private val buttonSpacing = (8 * density).toInt()
|
|
50
|
+
|
|
51
|
+
private val toolbarBackgroundColor = Color.parseColor("#2D2D2D")
|
|
52
|
+
private val activeColor = Color.parseColor("#5082C8")
|
|
53
|
+
private val inactiveColor = Color.WHITE
|
|
54
|
+
|
|
55
|
+
private val buttons = mutableMapOf<String, TextView>()
|
|
56
|
+
private val buttonContainer: LinearLayout
|
|
57
|
+
private val scrollView: HorizontalScrollView
|
|
58
|
+
|
|
59
|
+
private var enabledOptions: List<String> = listOf(
|
|
60
|
+
"bold", "italic", "underline", "strikethrough", "code", "highlight",
|
|
61
|
+
"heading", "bullet", "numbered", "quote", "checklist",
|
|
62
|
+
"link", "undo", "redo", "clearFormatting",
|
|
63
|
+
"indent", "outdent",
|
|
64
|
+
"alignLeft", "alignCenter", "alignRight"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
init {
|
|
68
|
+
orientation = HORIZONTAL
|
|
69
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
70
|
+
setPadding((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
|
|
71
|
+
|
|
72
|
+
// Set background with rounded corners and shadow
|
|
73
|
+
val bg = GradientDrawable().apply {
|
|
74
|
+
setColor(toolbarBackgroundColor)
|
|
75
|
+
cornerRadius = 10 * density
|
|
76
|
+
}
|
|
77
|
+
background = bg
|
|
78
|
+
elevation = 6 * density
|
|
79
|
+
|
|
80
|
+
// Left arrow
|
|
81
|
+
val leftArrow = createArrowButton("‹").apply {
|
|
82
|
+
setOnClickListener { scrollLeft() }
|
|
83
|
+
}
|
|
84
|
+
addView(leftArrow)
|
|
85
|
+
|
|
86
|
+
// Scroll view for buttons
|
|
87
|
+
scrollView = HorizontalScrollView(context).apply {
|
|
88
|
+
isHorizontalScrollBarEnabled = false
|
|
89
|
+
layoutParams = LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
buttonContainer = LinearLayout(context).apply {
|
|
93
|
+
orientation = HORIZONTAL
|
|
94
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
scrollView.addView(buttonContainer)
|
|
98
|
+
addView(scrollView)
|
|
99
|
+
|
|
100
|
+
// Right arrow
|
|
101
|
+
val rightArrow = createArrowButton("›").apply {
|
|
102
|
+
setOnClickListener { scrollRight() }
|
|
103
|
+
}
|
|
104
|
+
addView(rightArrow)
|
|
105
|
+
|
|
106
|
+
buildButtons()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun createArrowButton(text: String): TextView {
|
|
110
|
+
return TextView(context).apply {
|
|
111
|
+
this.text = text
|
|
112
|
+
textSize = 24f
|
|
113
|
+
setTextColor(Color.parseColor("#FFFFFF"))
|
|
114
|
+
gravity = Gravity.CENTER
|
|
115
|
+
isClickable = true
|
|
116
|
+
isFocusable = true
|
|
117
|
+
val params = LayoutParams((32 * density).toInt(), ViewGroup.LayoutParams.MATCH_PARENT)
|
|
118
|
+
layoutParams = params
|
|
119
|
+
// Add touch feedback
|
|
120
|
+
val bg = GradientDrawable().apply {
|
|
121
|
+
cornerRadius = 4 * density
|
|
122
|
+
setColor(Color.TRANSPARENT)
|
|
123
|
+
}
|
|
124
|
+
background = bg
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun scrollLeft() {
|
|
129
|
+
val scrollAmount = (150 * density).toInt()
|
|
130
|
+
scrollView.smoothScrollBy(-scrollAmount, 0)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun scrollRight() {
|
|
134
|
+
val scrollAmount = (150 * density).toInt()
|
|
135
|
+
scrollView.smoothScrollBy(scrollAmount, 0)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun setToolbarOptions(options: List<String>?) {
|
|
139
|
+
enabledOptions = options ?: listOf(
|
|
140
|
+
"bold", "italic", "underline", "strikethrough", "code", "highlight",
|
|
141
|
+
"heading", "bullet", "numbered", "quote", "checklist",
|
|
142
|
+
"link", "undo", "redo", "clearFormatting",
|
|
143
|
+
"indent", "outdent",
|
|
144
|
+
"alignLeft", "alignCenter", "alignRight"
|
|
145
|
+
)
|
|
146
|
+
buildButtons()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun buildButtons() {
|
|
150
|
+
buttonContainer.removeAllViews()
|
|
151
|
+
buttons.clear()
|
|
152
|
+
|
|
153
|
+
for (option in enabledOptions) {
|
|
154
|
+
val button = createButton(option)
|
|
155
|
+
if (button != null) {
|
|
156
|
+
buttons[option] = button
|
|
157
|
+
buttonContainer.addView(button)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private fun createButton(option: String): TextView? {
|
|
163
|
+
val button = TextView(context).apply {
|
|
164
|
+
gravity = Gravity.CENTER
|
|
165
|
+
setTextColor(inactiveColor)
|
|
166
|
+
|
|
167
|
+
val bg = GradientDrawable().apply {
|
|
168
|
+
cornerRadius = 6 * density
|
|
169
|
+
setColor(Color.TRANSPARENT)
|
|
170
|
+
}
|
|
171
|
+
background = bg
|
|
172
|
+
|
|
173
|
+
val params = LayoutParams(buttonSize, buttonSize)
|
|
174
|
+
params.marginEnd = buttonSpacing
|
|
175
|
+
layoutParams = params
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
when (option) {
|
|
179
|
+
"bold" -> {
|
|
180
|
+
button.text = "B"
|
|
181
|
+
button.textSize = 18f
|
|
182
|
+
button.setTypeface(null, Typeface.BOLD)
|
|
183
|
+
button.setOnClickListener { listener?.onBoldClick() }
|
|
184
|
+
}
|
|
185
|
+
"italic" -> {
|
|
186
|
+
button.text = "I"
|
|
187
|
+
button.textSize = 18f
|
|
188
|
+
button.setTypeface(null, Typeface.ITALIC)
|
|
189
|
+
button.setOnClickListener { listener?.onItalicClick() }
|
|
190
|
+
}
|
|
191
|
+
"underline" -> {
|
|
192
|
+
val spannable = SpannableString("U")
|
|
193
|
+
spannable.setSpan(UnderlineSpan(), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
194
|
+
button.text = spannable
|
|
195
|
+
button.textSize = 18f
|
|
196
|
+
button.setOnClickListener { listener?.onUnderlineClick() }
|
|
197
|
+
}
|
|
198
|
+
"strikethrough" -> {
|
|
199
|
+
val spannable = SpannableString("S")
|
|
200
|
+
spannable.setSpan(StrikethroughSpan(), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
201
|
+
button.text = spannable
|
|
202
|
+
button.textSize = 18f
|
|
203
|
+
button.setOnClickListener { listener?.onStrikethroughClick() }
|
|
204
|
+
}
|
|
205
|
+
"code" -> {
|
|
206
|
+
button.text = "</>"
|
|
207
|
+
button.textSize = 12f
|
|
208
|
+
button.setTypeface(Typeface.MONOSPACE, Typeface.NORMAL)
|
|
209
|
+
button.setOnClickListener { listener?.onCodeClick() }
|
|
210
|
+
}
|
|
211
|
+
"highlight" -> {
|
|
212
|
+
// Use marker/highlighter icon representation
|
|
213
|
+
button.text = "✎"
|
|
214
|
+
button.textSize = 18f
|
|
215
|
+
button.setOnClickListener { listener?.onHighlightClick() }
|
|
216
|
+
}
|
|
217
|
+
"heading" -> {
|
|
218
|
+
button.text = "H1"
|
|
219
|
+
button.textSize = 14f
|
|
220
|
+
button.setTypeface(null, Typeface.BOLD)
|
|
221
|
+
button.setOnClickListener { listener?.onHeadingClick() }
|
|
222
|
+
}
|
|
223
|
+
"bullet" -> {
|
|
224
|
+
button.text = "•≡"
|
|
225
|
+
button.textSize = 14f
|
|
226
|
+
button.setOnClickListener { listener?.onBulletListClick() }
|
|
227
|
+
}
|
|
228
|
+
"numbered" -> {
|
|
229
|
+
button.text = "1."
|
|
230
|
+
button.textSize = 14f
|
|
231
|
+
button.setOnClickListener { listener?.onNumberedListClick() }
|
|
232
|
+
}
|
|
233
|
+
"quote" -> {
|
|
234
|
+
button.text = "❞"
|
|
235
|
+
button.textSize = 18f
|
|
236
|
+
button.setOnClickListener { listener?.onQuoteClick() }
|
|
237
|
+
}
|
|
238
|
+
"checklist" -> {
|
|
239
|
+
button.text = "☑"
|
|
240
|
+
button.textSize = 18f
|
|
241
|
+
button.setOnClickListener { listener?.onChecklistClick() }
|
|
242
|
+
}
|
|
243
|
+
"link" -> {
|
|
244
|
+
button.text = "🔗"
|
|
245
|
+
button.textSize = 14f
|
|
246
|
+
button.setOnClickListener { listener?.onLinkClick() }
|
|
247
|
+
}
|
|
248
|
+
"undo" -> {
|
|
249
|
+
button.text = "↩"
|
|
250
|
+
button.textSize = 18f
|
|
251
|
+
button.setOnClickListener { listener?.onUndoClick() }
|
|
252
|
+
}
|
|
253
|
+
"redo" -> {
|
|
254
|
+
button.text = "↪"
|
|
255
|
+
button.textSize = 18f
|
|
256
|
+
button.setOnClickListener { listener?.onRedoClick() }
|
|
257
|
+
}
|
|
258
|
+
"clearFormatting" -> {
|
|
259
|
+
val spannable = SpannableString("Tx")
|
|
260
|
+
spannable.setSpan(StrikethroughSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
261
|
+
button.text = spannable
|
|
262
|
+
button.textSize = 14f
|
|
263
|
+
button.setOnClickListener { listener?.onClearFormattingClick() }
|
|
264
|
+
}
|
|
265
|
+
"indent" -> {
|
|
266
|
+
button.text = "→⊢"
|
|
267
|
+
button.textSize = 12f
|
|
268
|
+
button.setOnClickListener { listener?.onIndentClick() }
|
|
269
|
+
}
|
|
270
|
+
"outdent" -> {
|
|
271
|
+
button.text = "⊣←"
|
|
272
|
+
button.textSize = 12f
|
|
273
|
+
button.setOnClickListener { listener?.onOutdentClick() }
|
|
274
|
+
}
|
|
275
|
+
"alignLeft" -> {
|
|
276
|
+
button.text = "≡"
|
|
277
|
+
button.textSize = 18f
|
|
278
|
+
button.gravity = Gravity.START or Gravity.CENTER_VERTICAL
|
|
279
|
+
button.setOnClickListener { listener?.onAlignLeftClick() }
|
|
280
|
+
}
|
|
281
|
+
"alignCenter" -> {
|
|
282
|
+
button.text = "≡"
|
|
283
|
+
button.textSize = 18f
|
|
284
|
+
button.gravity = Gravity.CENTER
|
|
285
|
+
button.setOnClickListener { listener?.onAlignCenterClick() }
|
|
286
|
+
}
|
|
287
|
+
"alignRight" -> {
|
|
288
|
+
button.text = "≡"
|
|
289
|
+
button.textSize = 18f
|
|
290
|
+
button.gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
291
|
+
button.setOnClickListener { listener?.onAlignRightClick() }
|
|
292
|
+
}
|
|
293
|
+
else -> return null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return button
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fun updateButtonStates(
|
|
300
|
+
bold: Boolean = false,
|
|
301
|
+
italic: Boolean = false,
|
|
302
|
+
underline: Boolean = false,
|
|
303
|
+
strikethrough: Boolean = false,
|
|
304
|
+
code: Boolean = false,
|
|
305
|
+
highlight: Boolean = false,
|
|
306
|
+
heading: Boolean = false,
|
|
307
|
+
bullet: Boolean = false,
|
|
308
|
+
numbered: Boolean = false,
|
|
309
|
+
quote: Boolean = false,
|
|
310
|
+
checklist: Boolean = false,
|
|
311
|
+
alignLeft: Boolean = true,
|
|
312
|
+
alignCenter: Boolean = false,
|
|
313
|
+
alignRight: Boolean = false
|
|
314
|
+
) {
|
|
315
|
+
val states = mapOf(
|
|
316
|
+
"bold" to bold,
|
|
317
|
+
"italic" to italic,
|
|
318
|
+
"underline" to underline,
|
|
319
|
+
"strikethrough" to strikethrough,
|
|
320
|
+
"code" to code,
|
|
321
|
+
"highlight" to highlight,
|
|
322
|
+
"heading" to heading,
|
|
323
|
+
"bullet" to bullet,
|
|
324
|
+
"numbered" to numbered,
|
|
325
|
+
"quote" to quote,
|
|
326
|
+
"checklist" to checklist,
|
|
327
|
+
"alignLeft" to alignLeft,
|
|
328
|
+
"alignCenter" to alignCenter,
|
|
329
|
+
"alignRight" to alignRight
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
for ((option, button) in buttons) {
|
|
333
|
+
val isActive = states[option] ?: false
|
|
334
|
+
button.setTextColor(if (isActive) activeColor else inactiveColor)
|
|
335
|
+
(button.background as? GradientDrawable)?.setColor(
|
|
336
|
+
if (isActive) Color.parseColor("#40FFFFFF") else Color.TRANSPARENT
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fun getToolbarWidth(): Int {
|
|
342
|
+
val screenWidth = context.resources.displayMetrics.widthPixels
|
|
343
|
+
val maxWidth = (screenWidth * 0.9).toInt()
|
|
344
|
+
val buttonCount = enabledOptions.size
|
|
345
|
+
val calculatedWidth = (buttonCount * buttonSize) + ((buttonCount - 1) * buttonSpacing) + (56 * density).toInt()
|
|
346
|
+
return minOf(calculatedWidth, maxWidth)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fun getToolbarHeight(): Int = toolbarHeight
|
|
350
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.richtext.editor
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class RichTextEditorPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return emptyList()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return listOf(RichTextEditorViewManager())
|
|
15
|
+
}
|
|
16
|
+
}
|