@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.
- package/LICENSE +160 -0
- package/README.md +180 -0
- package/android/build.gradle +69 -0
- package/android/consumer-rules.pro +18 -0
- package/android/src/androidTest/AndroidManifest.xml +8 -0
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
- package/app.plugin.js +62 -0
- package/dist/EditorTheme.d.ts +113 -0
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +191 -0
- package/dist/EditorToolbar.js +1095 -0
- package/dist/NativeEditorBridge.d.ts +325 -0
- package/dist/NativeEditorBridge.js +933 -0
- package/dist/NativeProseViewer.d.ts +53 -0
- package/dist/NativeProseViewer.js +480 -0
- package/dist/NativeRichTextEditor.d.ts +183 -0
- package/dist/NativeRichTextEditor.js +2574 -0
- package/dist/YjsCollaboration.d.ts +90 -0
- package/dist/YjsCollaboration.js +743 -0
- package/dist/addons.d.ts +100 -0
- package/dist/addons.js +82 -0
- package/dist/heightCache.d.ts +6 -0
- package/dist/heightCache.js +45 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +30 -0
- package/dist/schemas.d.ts +37 -0
- package/dist/schemas.js +263 -0
- package/dist/useNativeEditor.d.ts +42 -0
- package/dist/useNativeEditor.js +124 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +327 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- 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 +774 -0
- package/ios/EditorTheme.swift +515 -0
- package/ios/Generated_editor_core.swift +1513 -0
- package/ios/NativeEditorExpoView.swift +3593 -0
- package/ios/NativeEditorModule.swift +624 -0
- package/ios/NativeProseViewerExpoView.swift +276 -0
- package/ios/PositionBridge.swift +558 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +1708 -0
- package/ios/RichTextEditorView.swift +6150 -0
- package/ios/Tests/NativePerformanceTests.swift +687 -0
- package/ios/Tests/PositionBridgeTests.swift +706 -0
- package/ios/Tests/RenderBridgeTests.swift +2236 -0
- package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +74 -0
- 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 +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
|
+
}
|