@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.
Files changed (53) hide show
  1. package/README.md +12 -7
  2. package/android/build.gradle +7 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
  5. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
  9. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
  10. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
  12. package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
  13. package/dist/EditorTheme.js +29 -0
  14. package/dist/EditorToolbar.d.ts +129 -0
  15. package/dist/EditorToolbar.js +394 -0
  16. package/dist/NativeEditorBridge.d.ts +242 -0
  17. package/dist/NativeEditorBridge.js +647 -0
  18. package/dist/NativeRichTextEditor.d.ts +142 -0
  19. package/dist/NativeRichTextEditor.js +649 -0
  20. package/dist/YjsCollaboration.d.ts +83 -0
  21. package/dist/YjsCollaboration.js +585 -0
  22. package/dist/addons.d.ts +70 -0
  23. package/dist/addons.js +77 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +26 -0
  26. package/dist/schemas.d.ts +35 -0
  27. package/{src/schemas.ts → dist/schemas.js} +62 -27
  28. package/dist/useNativeEditor.d.ts +40 -0
  29. package/dist/useNativeEditor.js +117 -0
  30. package/ios/EditorAddons.swift +26 -3
  31. package/ios/EditorCore.xcframework/Info.plist +5 -5
  32. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  33. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  34. package/ios/EditorLayoutManager.swift +236 -0
  35. package/ios/EditorTheme.swift +51 -1
  36. package/ios/Generated_editor_core.swift +270 -2
  37. package/ios/NativeEditorExpoView.swift +612 -45
  38. package/ios/NativeEditorModule.swift +81 -0
  39. package/ios/PositionBridge.swift +22 -0
  40. package/ios/RenderBridge.swift +427 -39
  41. package/ios/RichTextEditorView.swift +1342 -18
  42. package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
  43. package/package.json +80 -64
  44. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  45. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  46. package/rust/android/x86_64/libeditor_core.so +0 -0
  47. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
  48. package/src/EditorToolbar.tsx +0 -620
  49. package/src/NativeEditorBridge.ts +0 -607
  50. package/src/NativeRichTextEditor.tsx +0 -951
  51. package/src/addons.ts +0 -158
  52. package/src/index.ts +0 -63
  53. 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
+ }