@openeditor/react-native-prose-editor 0.0.2

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 (91) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +180 -0
  3. package/android/build.gradle +69 -0
  4. package/android/consumer-rules.pro +18 -0
  5. package/android/src/androidTest/AndroidManifest.xml +8 -0
  6. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
  7. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
  8. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
  9. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
  10. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
  11. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
  12. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  13. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  14. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
  15. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
  16. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
  17. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  18. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
  19. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
  20. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  21. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
  22. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
  23. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
  24. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
  25. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
  26. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
  27. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
  28. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
  29. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
  30. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
  31. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
  32. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
  33. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
  34. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
  35. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
  36. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
  37. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
  38. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
  39. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
  40. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
  41. package/app.plugin.js +62 -0
  42. package/dist/EditorTheme.d.ts +113 -0
  43. package/dist/EditorTheme.js +29 -0
  44. package/dist/EditorToolbar.d.ts +191 -0
  45. package/dist/EditorToolbar.js +1095 -0
  46. package/dist/NativeEditorBridge.d.ts +325 -0
  47. package/dist/NativeEditorBridge.js +933 -0
  48. package/dist/NativeProseViewer.d.ts +53 -0
  49. package/dist/NativeProseViewer.js +480 -0
  50. package/dist/NativeRichTextEditor.d.ts +183 -0
  51. package/dist/NativeRichTextEditor.js +2574 -0
  52. package/dist/YjsCollaboration.d.ts +90 -0
  53. package/dist/YjsCollaboration.js +743 -0
  54. package/dist/addons.d.ts +100 -0
  55. package/dist/addons.js +82 -0
  56. package/dist/heightCache.d.ts +6 -0
  57. package/dist/heightCache.js +45 -0
  58. package/dist/index.d.ts +10 -0
  59. package/dist/index.js +30 -0
  60. package/dist/schemas.d.ts +37 -0
  61. package/dist/schemas.js +263 -0
  62. package/dist/useNativeEditor.d.ts +42 -0
  63. package/dist/useNativeEditor.js +124 -0
  64. package/expo-module.config.json +9 -0
  65. package/ios/EditorAddons.swift +327 -0
  66. package/ios/EditorCore.xcframework/Info.plist +44 -0
  67. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  68. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  69. package/ios/EditorLayoutManager.swift +774 -0
  70. package/ios/EditorTheme.swift +515 -0
  71. package/ios/Generated_editor_core.swift +1513 -0
  72. package/ios/NativeEditorExpoView.swift +3593 -0
  73. package/ios/NativeEditorModule.swift +624 -0
  74. package/ios/NativeProseViewerExpoView.swift +276 -0
  75. package/ios/PositionBridge.swift +558 -0
  76. package/ios/ReactNativeProseEditor.podspec +49 -0
  77. package/ios/RenderBridge.swift +1708 -0
  78. package/ios/RichTextEditorView.swift +6150 -0
  79. package/ios/Tests/NativePerformanceTests.swift +687 -0
  80. package/ios/Tests/PositionBridgeTests.swift +706 -0
  81. package/ios/Tests/RenderBridgeTests.swift +2236 -0
  82. package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
  83. package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
  84. package/ios/editor_coreFFI/module.modulemap +7 -0
  85. package/ios/editor_coreFFI.h +904 -0
  86. package/ios/editor_coreFFI.modulemap +7 -0
  87. package/package.json +74 -0
  88. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  89. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  90. package/rust/android/x86_64/libeditor_core.so +0 -0
  91. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
@@ -0,0 +1,315 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.content.Intent
4
+ import android.content.Context
5
+ import android.graphics.Color
6
+ import android.net.Uri
7
+ import android.view.MotionEvent
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import expo.modules.kotlin.AppContext
11
+ import expo.modules.kotlin.viewevent.EventDispatcher
12
+ import expo.modules.kotlin.views.ExpoView
13
+ import kotlin.math.abs
14
+ import org.json.JSONArray
15
+
16
+ class NativeProseViewerExpoView(
17
+ context: Context,
18
+ appContext: AppContext
19
+ ) : ExpoView(context, appContext) {
20
+
21
+ private val proseView = EditorEditText(context)
22
+ private val onContentHeightChange by EventDispatcher<Map<String, Any>>()
23
+ @Suppress("unused")
24
+ private val onPressLink by EventDispatcher<Map<String, Any>>()
25
+ @Suppress("unused")
26
+ private val onPressMention by EventDispatcher<Map<String, Any>>()
27
+
28
+ private var lastRenderJson: String? = null
29
+ private var lastThemeJson: String? = null
30
+ private var lastEmittedContentHeight = 0
31
+ private var collapsesWhenEmpty = true
32
+ private var isCollapsedEmptyContent = false
33
+ private var enableLinkTaps = true
34
+ private var interceptLinkTaps = false
35
+ internal var suppressContentHeightEventsForTesting = false
36
+
37
+ init {
38
+ proseView.setBaseStyle(
39
+ proseView.textSize,
40
+ proseView.currentTextColor,
41
+ Color.TRANSPARENT
42
+ )
43
+ proseView.isEditable = false
44
+ proseView.inputType = android.text.InputType.TYPE_CLASS_TEXT or
45
+ android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE or
46
+ android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
47
+ proseView.setImageResizingEnabled(false)
48
+ proseView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
49
+ proseView.isFocusable = false
50
+ proseView.isFocusableInTouchMode = false
51
+ proseView.isCursorVisible = false
52
+ proseView.isLongClickable = false
53
+ proseView.setTextIsSelectable(false)
54
+ proseView.showSoftInputOnFocus = false
55
+ proseView.setOnTouchListener { _, event ->
56
+ if (event.actionMasked != MotionEvent.ACTION_UP) {
57
+ return@setOnTouchListener false
58
+ }
59
+
60
+ proseView.mentionHitAt(event.x, event.y)?.let { mention ->
61
+ onPressMention(mapOf("docPos" to mention.docPos, "label" to mention.label))
62
+ return@setOnTouchListener true
63
+ }
64
+
65
+ if (!enableLinkTaps) {
66
+ return@setOnTouchListener false
67
+ }
68
+
69
+ val link = proseView.linkHitAt(event.x, event.y) ?: return@setOnTouchListener false
70
+ if (interceptLinkTaps) {
71
+ onPressLink(
72
+ mapOf(
73
+ "href" to link.href,
74
+ "text" to link.text
75
+ )
76
+ )
77
+ return@setOnTouchListener true
78
+ }
79
+
80
+ return@setOnTouchListener openLink(link.href)
81
+ }
82
+
83
+ addView(
84
+ proseView,
85
+ LayoutParams(
86
+ ViewGroup.LayoutParams.MATCH_PARENT,
87
+ ViewGroup.LayoutParams.WRAP_CONTENT
88
+ )
89
+ )
90
+ }
91
+
92
+ fun setRenderJson(renderJson: String?) {
93
+ if (lastRenderJson == renderJson) return
94
+ lastRenderJson = renderJson
95
+ applyRenderJson()
96
+ requestLayout()
97
+ }
98
+
99
+ fun setThemeJson(themeJson: String?) {
100
+ if (lastThemeJson == themeJson) return
101
+ lastThemeJson = themeJson
102
+ proseView.applyTheme(EditorTheme.fromJson(themeJson))
103
+ applyRenderJson()
104
+ requestLayout()
105
+ }
106
+
107
+ fun setCollapsesWhenEmpty(collapsesWhenEmpty: Boolean?) {
108
+ val nextValue = collapsesWhenEmpty ?: true
109
+ if (this.collapsesWhenEmpty == nextValue) return
110
+ this.collapsesWhenEmpty = nextValue
111
+ updateCollapsedEmptyState()
112
+ requestLayout()
113
+ emitContentHeightIfNeeded(force = true)
114
+ }
115
+
116
+ fun setEnableLinkTaps(enableLinkTaps: Boolean?) {
117
+ this.enableLinkTaps = enableLinkTaps ?: true
118
+ }
119
+
120
+ fun setInterceptLinkTaps(interceptLinkTaps: Boolean?) {
121
+ this.interceptLinkTaps = interceptLinkTaps ?: false
122
+ }
123
+
124
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
125
+ if (isCollapsedEmptyContent) {
126
+ setMeasuredDimension(resolveSize(0, widthMeasureSpec), 0)
127
+ emitContentHeightIfNeeded()
128
+ return
129
+ }
130
+
131
+ val childWidthSpec = getChildMeasureSpec(
132
+ widthMeasureSpec,
133
+ paddingLeft + paddingRight,
134
+ proseView.layoutParams.width
135
+ )
136
+ val childHeightSpec = android.view.View.MeasureSpec.makeMeasureSpec(
137
+ 0,
138
+ android.view.View.MeasureSpec.UNSPECIFIED
139
+ )
140
+ proseView.measure(childWidthSpec, childHeightSpec)
141
+
142
+ val resolvedContentHeight = proseView.resolveAutoGrowHeight()
143
+ val desiredWidth = proseView.measuredWidth + paddingLeft + paddingRight
144
+ val desiredHeight = resolvedContentHeight + paddingTop + paddingBottom
145
+ val measuredHeight = when (View.MeasureSpec.getMode(heightMeasureSpec)) {
146
+ View.MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(
147
+ View.MeasureSpec.getSize(heightMeasureSpec)
148
+ )
149
+ else -> desiredHeight
150
+ }
151
+ setMeasuredDimension(
152
+ resolveSize(desiredWidth, widthMeasureSpec),
153
+ measuredHeight
154
+ )
155
+ emitContentHeightIfNeeded(measuredContentHeight = desiredHeight)
156
+ }
157
+
158
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
159
+ if (isCollapsedEmptyContent) {
160
+ proseView.layout(paddingLeft, paddingTop, right - left - paddingRight, paddingTop)
161
+ emitContentHeightIfNeeded()
162
+ return
163
+ }
164
+
165
+ val childLeft = paddingLeft
166
+ val childTop = paddingTop
167
+ proseView.layout(
168
+ childLeft,
169
+ childTop,
170
+ right - left - paddingRight,
171
+ childTop + proseView.measuredHeight
172
+ )
173
+ emitContentHeightIfNeeded()
174
+ }
175
+
176
+ private fun applyRenderJson() {
177
+ updateCollapsedEmptyState()
178
+ proseView.applyRenderJSON(lastRenderJson ?: "[]")
179
+ proseView.visibility = if (isCollapsedEmptyContent) View.GONE else View.VISIBLE
180
+ }
181
+
182
+ private fun updateCollapsedEmptyState() {
183
+ isCollapsedEmptyContent = collapsesWhenEmpty &&
184
+ renderJsonContainsOnlyEmptyParagraphs(lastRenderJson ?: "[]")
185
+ proseView.visibility = if (isCollapsedEmptyContent) View.GONE else View.VISIBLE
186
+ }
187
+
188
+ private fun emitContentHeightIfNeeded(
189
+ force: Boolean = false,
190
+ measuredContentHeight: Int? = null
191
+ ) {
192
+ val contentHeight = if (isCollapsedEmptyContent) {
193
+ 0
194
+ } else {
195
+ (
196
+ measuredContentHeight ?: (measureContentHeightPx() + paddingTop + paddingBottom)
197
+ ).coerceAtLeast(0)
198
+ }
199
+ if (contentHeight <= 0 && !isCollapsedEmptyContent) {
200
+ return
201
+ }
202
+ if (!force && contentHeight == lastEmittedContentHeight) {
203
+ return
204
+ }
205
+ lastEmittedContentHeight = contentHeight
206
+ if (suppressContentHeightEventsForTesting) {
207
+ return
208
+ }
209
+ onContentHeightChange(mapOf("contentHeight" to contentHeight))
210
+ }
211
+
212
+ private fun measureContentHeightPx(): Int {
213
+ if (isCollapsedEmptyContent) {
214
+ return 0
215
+ }
216
+
217
+ val availableWidthPx = resolveAvailableWidthPx()
218
+ if (
219
+ proseView.measuredWidth <= 0 ||
220
+ abs(proseView.measuredWidth - availableWidthPx) > 1
221
+ ) {
222
+ val childWidthSpec = View.MeasureSpec.makeMeasureSpec(
223
+ availableWidthPx,
224
+ View.MeasureSpec.EXACTLY
225
+ )
226
+ val childHeightSpec = View.MeasureSpec.makeMeasureSpec(
227
+ 0,
228
+ View.MeasureSpec.UNSPECIFIED
229
+ )
230
+ proseView.measure(childWidthSpec, childHeightSpec)
231
+ }
232
+ return proseView.resolveAutoGrowHeight()
233
+ }
234
+
235
+ private fun resolveAvailableWidthPx(): Int {
236
+ val localWidth = width - paddingLeft - paddingRight
237
+ if (localWidth > 0) {
238
+ return localWidth
239
+ }
240
+
241
+ val parentWidth = ((parent as? View)?.width ?: 0) - paddingLeft - paddingRight
242
+ if (parentWidth > 0) {
243
+ return parentWidth
244
+ }
245
+
246
+ return (resources.displayMetrics.widthPixels - paddingLeft - paddingRight).coerceAtLeast(1)
247
+ }
248
+
249
+ private fun openLink(href: String): Boolean {
250
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(href)).apply {
251
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
252
+ }
253
+ return runCatching {
254
+ context.startActivity(intent)
255
+ true
256
+ }.getOrDefault(false)
257
+ }
258
+
259
+ companion object {
260
+ private const val EMPTY_TEXT_BLOCK_PLACEHOLDER = '\u200B'
261
+
262
+ internal fun renderJsonContainsOnlyEmptyParagraphs(renderJson: String): Boolean {
263
+ val elements = try {
264
+ JSONArray(renderJson)
265
+ } catch (_: Exception) {
266
+ return false
267
+ }
268
+
269
+ if (elements.length() == 0) {
270
+ return true
271
+ }
272
+
273
+ var hasParagraph = false
274
+ var paragraphIsOpen = false
275
+
276
+ for (index in 0 until elements.length()) {
277
+ val element = elements.optJSONObject(index) ?: return false
278
+ when (element.optString("type", "")) {
279
+ "blockStart" -> {
280
+ if (
281
+ paragraphIsOpen ||
282
+ element.optString("nodeType", "") != "paragraph" ||
283
+ element.optInt("depth", 0) != 0
284
+ ) {
285
+ return false
286
+ }
287
+ paragraphIsOpen = true
288
+ hasParagraph = true
289
+ }
290
+
291
+ "textRun" -> {
292
+ val text = element.optString("text", "")
293
+ if (
294
+ !paragraphIsOpen ||
295
+ !text.all { it == EMPTY_TEXT_BLOCK_PLACEHOLDER }
296
+ ) {
297
+ return false
298
+ }
299
+ }
300
+
301
+ "blockEnd" -> {
302
+ if (!paragraphIsOpen) {
303
+ return false
304
+ }
305
+ paragraphIsOpen = false
306
+ }
307
+
308
+ else -> return false
309
+ }
310
+ }
311
+
312
+ return hasParagraph && !paragraphIsOpen
313
+ }
314
+ }
315
+ }