@apollohg/react-native-prose-editor 0.1.1 → 0.3.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/README.md +12 -7
- package/android/build.gradle +7 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
- package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +129 -0
- package/dist/EditorToolbar.js +394 -0
- package/dist/NativeEditorBridge.d.ts +242 -0
- package/dist/NativeEditorBridge.js +647 -0
- package/dist/NativeRichTextEditor.d.ts +142 -0
- package/dist/NativeRichTextEditor.js +649 -0
- package/dist/YjsCollaboration.d.ts +83 -0
- package/dist/YjsCollaboration.js +585 -0
- package/dist/addons.d.ts +70 -0
- package/dist/addons.js +77 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +26 -0
- package/dist/schemas.d.ts +35 -0
- package/{src/schemas.ts → dist/schemas.js} +62 -27
- package/dist/useNativeEditor.d.ts +40 -0
- package/dist/useNativeEditor.js +117 -0
- package/ios/EditorAddons.swift +26 -3
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +236 -0
- package/ios/EditorTheme.swift +51 -1
- package/ios/Generated_editor_core.swift +270 -2
- package/ios/NativeEditorExpoView.swift +612 -45
- package/ios/NativeEditorModule.swift +81 -0
- package/ios/PositionBridge.swift +22 -0
- package/ios/RenderBridge.swift +427 -39
- package/ios/RichTextEditorView.swift +1342 -18
- package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
- package/package.json +80 -64
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
- package/src/EditorToolbar.tsx +0 -620
- package/src/NativeEditorBridge.ts +0 -607
- package/src/NativeRichTextEditor.tsx +0 -951
- package/src/addons.ts +0 -158
- package/src/index.ts +0 -63
- package/src/useNativeEditor.ts +0 -173
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
package com.apollohg.editor
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.Path
|
|
8
|
+
import android.graphics.RectF
|
|
9
|
+
import android.util.AttributeSet
|
|
10
|
+
import android.util.TypedValue
|
|
11
|
+
import android.view.View
|
|
12
|
+
import androidx.appcompat.content.res.AppCompatResources
|
|
13
|
+
import com.google.android.material.R as MaterialR
|
|
14
|
+
import org.json.JSONArray
|
|
15
|
+
import uniffi.editor_core.editorDocToScalar
|
|
16
|
+
|
|
17
|
+
data class RemoteSelectionDecoration(
|
|
18
|
+
val clientId: Int,
|
|
19
|
+
val anchor: Int,
|
|
20
|
+
val head: Int,
|
|
21
|
+
val color: Int,
|
|
22
|
+
val name: String?,
|
|
23
|
+
val isFocused: Boolean,
|
|
24
|
+
) {
|
|
25
|
+
companion object {
|
|
26
|
+
fun fromJson(context: Context, json: String?): List<RemoteSelectionDecoration> {
|
|
27
|
+
if (json.isNullOrBlank()) return emptyList()
|
|
28
|
+
val array = try {
|
|
29
|
+
JSONArray(json)
|
|
30
|
+
} catch (_: Throwable) {
|
|
31
|
+
return emptyList()
|
|
32
|
+
}
|
|
33
|
+
val fallbackColor = resolveFallbackColor(context)
|
|
34
|
+
|
|
35
|
+
return buildList {
|
|
36
|
+
for (index in 0 until array.length()) {
|
|
37
|
+
val item = array.optJSONObject(index) ?: continue
|
|
38
|
+
val color = parseColor(item.optString("color", ""), fallbackColor)
|
|
39
|
+
add(
|
|
40
|
+
RemoteSelectionDecoration(
|
|
41
|
+
clientId = item.optInt("clientId", 0),
|
|
42
|
+
anchor = item.optInt("anchor", 0),
|
|
43
|
+
head = item.optInt("head", 0),
|
|
44
|
+
color = color,
|
|
45
|
+
name = item.optString("name").takeIf { it.isNotBlank() },
|
|
46
|
+
isFocused = item.optBoolean("isFocused", false),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private fun parseColor(raw: String, fallbackColor: Int): Int {
|
|
54
|
+
return try {
|
|
55
|
+
Color.parseColor(raw)
|
|
56
|
+
} catch (_: Throwable) {
|
|
57
|
+
fallbackColor
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun resolveFallbackColor(context: Context): Int {
|
|
62
|
+
val typedValue = TypedValue()
|
|
63
|
+
val attrs = intArrayOf(
|
|
64
|
+
MaterialR.attr.colorPrimary,
|
|
65
|
+
MaterialR.attr.colorSecondary,
|
|
66
|
+
android.R.attr.colorAccent,
|
|
67
|
+
android.R.attr.textColorPrimary
|
|
68
|
+
)
|
|
69
|
+
for (attr in attrs) {
|
|
70
|
+
if (!context.theme.resolveAttribute(attr, typedValue, true)) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (typedValue.resourceId != 0) {
|
|
74
|
+
AppCompatResources.getColorStateList(context, typedValue.resourceId)
|
|
75
|
+
?.defaultColor
|
|
76
|
+
?.let { return it }
|
|
77
|
+
} else if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
|
|
78
|
+
return typedValue.data
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Color.TRANSPARENT
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
data class RemoteSelectionDebugSnapshot(
|
|
87
|
+
val clientId: Int,
|
|
88
|
+
val caretRect: RectF?,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
92
|
+
context: Context,
|
|
93
|
+
attrs: AttributeSet? = null,
|
|
94
|
+
defStyleAttr: Int = 0,
|
|
95
|
+
) : View(context, attrs, defStyleAttr) {
|
|
96
|
+
|
|
97
|
+
private var editorView: RichTextEditorView? = null
|
|
98
|
+
private var remoteSelections: List<RemoteSelectionDecoration> = emptyList()
|
|
99
|
+
internal var editorIdOverrideForTesting: Long? = null
|
|
100
|
+
internal var docToScalarResolver: (Long, Int) -> Int = { editorId, docPos ->
|
|
101
|
+
editorDocToScalar(editorId.toULong(), docPos.toUInt()).toInt()
|
|
102
|
+
}
|
|
103
|
+
private val selectionPath = Path()
|
|
104
|
+
private val selectionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
105
|
+
style = Paint.Style.FILL
|
|
106
|
+
}
|
|
107
|
+
private val caretPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
108
|
+
style = Paint.Style.FILL
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
init {
|
|
112
|
+
setWillNotDraw(false)
|
|
113
|
+
isClickable = false
|
|
114
|
+
isFocusable = false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fun bind(editorView: RichTextEditorView) {
|
|
118
|
+
this.editorView = editorView
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fun setRemoteSelections(selections: List<RemoteSelectionDecoration>) {
|
|
122
|
+
remoteSelections = selections
|
|
123
|
+
invalidate()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fun debugSnapshotsForTesting(): List<RemoteSelectionDebugSnapshot> {
|
|
127
|
+
val editorView = editorView ?: return emptyList()
|
|
128
|
+
val editorId = resolvedEditorId(editorView)
|
|
129
|
+
if (editorId == 0L || remoteSelections.isEmpty()) return emptyList()
|
|
130
|
+
|
|
131
|
+
val editText = editorView.editorEditText
|
|
132
|
+
val layout = editText.layout ?: return emptyList()
|
|
133
|
+
val text = editText.text?.toString() ?: return emptyList()
|
|
134
|
+
val baseX = (editorView.editorViewport.left + editorView.editorScrollView.left + editText.left).toFloat() +
|
|
135
|
+
editText.compoundPaddingLeft
|
|
136
|
+
val baseY = (editorView.editorViewport.top + editorView.editorScrollView.top + editText.top).toFloat() +
|
|
137
|
+
editText.compoundPaddingTop - editorView.editorScrollView.scrollY
|
|
138
|
+
val caretWidth = maxOf(2f, 2f * resources.displayMetrics.density / 2f)
|
|
139
|
+
|
|
140
|
+
return remoteSelections.map { selection ->
|
|
141
|
+
RemoteSelectionDebugSnapshot(
|
|
142
|
+
clientId = selection.clientId,
|
|
143
|
+
caretRect = caretRectForSelection(
|
|
144
|
+
selection = selection,
|
|
145
|
+
editorId = editorId,
|
|
146
|
+
text = text,
|
|
147
|
+
layout = layout,
|
|
148
|
+
baseX = baseX,
|
|
149
|
+
baseY = baseY,
|
|
150
|
+
caretWidth = caretWidth,
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
override fun onDraw(canvas: Canvas) {
|
|
157
|
+
super.onDraw(canvas)
|
|
158
|
+
|
|
159
|
+
val editorView = editorView ?: return
|
|
160
|
+
val editorId = resolvedEditorId(editorView)
|
|
161
|
+
if (editorId == 0L || remoteSelections.isEmpty()) return
|
|
162
|
+
|
|
163
|
+
val editText = editorView.editorEditText
|
|
164
|
+
val layout = editText.layout ?: return
|
|
165
|
+
val text = editText.text?.toString() ?: return
|
|
166
|
+
val baseX = (editorView.editorViewport.left + editorView.editorScrollView.left + editText.left).toFloat() +
|
|
167
|
+
editText.compoundPaddingLeft
|
|
168
|
+
val baseY = (editorView.editorViewport.top + editorView.editorScrollView.top + editText.top).toFloat() +
|
|
169
|
+
editText.compoundPaddingTop - editorView.editorScrollView.scrollY
|
|
170
|
+
val caretWidth = maxOf(2f, 2f * resources.displayMetrics.density / 2f)
|
|
171
|
+
|
|
172
|
+
for (selection in remoteSelections) {
|
|
173
|
+
val startDoc = minOf(selection.anchor, selection.head)
|
|
174
|
+
val endDoc = maxOf(selection.anchor, selection.head)
|
|
175
|
+
val startScalar = docToScalarResolver(editorId, startDoc)
|
|
176
|
+
val endScalar = docToScalarResolver(editorId, endDoc)
|
|
177
|
+
val startUtf16 = PositionBridge.scalarToUtf16(startScalar, text).coerceIn(0, text.length)
|
|
178
|
+
val endUtf16 = PositionBridge.scalarToUtf16(endScalar, text).coerceIn(0, text.length)
|
|
179
|
+
|
|
180
|
+
selectionPaint.color = withAlpha(selection.color, 0.18f)
|
|
181
|
+
caretPaint.color = selection.color
|
|
182
|
+
|
|
183
|
+
if (startUtf16 != endUtf16) {
|
|
184
|
+
selectionPath.reset()
|
|
185
|
+
layout.getSelectionPath(startUtf16, endUtf16, selectionPath)
|
|
186
|
+
canvas.save()
|
|
187
|
+
canvas.translate(baseX, baseY)
|
|
188
|
+
canvas.drawPath(selectionPath, selectionPaint)
|
|
189
|
+
canvas.restore()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!selection.isFocused) {
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
val caretRect = caretRectForSelection(
|
|
197
|
+
selection = selection,
|
|
198
|
+
editorId = editorId,
|
|
199
|
+
text = text,
|
|
200
|
+
layout = layout,
|
|
201
|
+
baseX = baseX,
|
|
202
|
+
baseY = baseY,
|
|
203
|
+
caretWidth = caretWidth,
|
|
204
|
+
) ?: continue
|
|
205
|
+
canvas.drawRoundRect(
|
|
206
|
+
caretRect.left,
|
|
207
|
+
caretRect.top,
|
|
208
|
+
caretRect.right,
|
|
209
|
+
caretRect.bottom,
|
|
210
|
+
caretWidth / 2f,
|
|
211
|
+
caretWidth / 2f,
|
|
212
|
+
caretPaint
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private fun caretRectForSelection(
|
|
218
|
+
selection: RemoteSelectionDecoration,
|
|
219
|
+
editorId: Long,
|
|
220
|
+
text: String,
|
|
221
|
+
layout: android.text.Layout,
|
|
222
|
+
baseX: Float,
|
|
223
|
+
baseY: Float,
|
|
224
|
+
caretWidth: Float,
|
|
225
|
+
): RectF? {
|
|
226
|
+
if (!selection.isFocused) return null
|
|
227
|
+
|
|
228
|
+
val endDoc = maxOf(selection.anchor, selection.head)
|
|
229
|
+
val endScalar = docToScalarResolver(editorId, endDoc)
|
|
230
|
+
val endUtf16 = PositionBridge.scalarToUtf16(endScalar, text).coerceIn(0, text.length)
|
|
231
|
+
val line = layout.getLineForOffset(endUtf16.coerceAtMost(maxOf(text.length - 1, 0)))
|
|
232
|
+
val horizontal = layout.getPrimaryHorizontal(endUtf16)
|
|
233
|
+
val caretLeft = baseX + horizontal
|
|
234
|
+
val caretTop = baseY + layout.getLineTop(line)
|
|
235
|
+
val caretBottom = baseY + layout.getLineBottom(line)
|
|
236
|
+
return RectF(caretLeft, caretTop, caretLeft + caretWidth, caretBottom)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private fun withAlpha(color: Int, alphaFraction: Float): Int {
|
|
240
|
+
val alpha = (255f * alphaFraction).toInt().coerceIn(0, 255)
|
|
241
|
+
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private fun resolvedEditorId(editorView: RichTextEditorView): Long =
|
|
245
|
+
editorIdOverrideForTesting ?: editorView.editorId
|
|
246
|
+
}
|