@openeditor/react-native-prose-editor 0.0.6 → 0.0.8
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/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- 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/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- package/package.json +1 -1
- 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 +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -165,6 +165,26 @@ class NativeEditorModule : Module() {
|
|
|
165
165
|
val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
|
|
166
166
|
editorOutdentListItem(editorId)
|
|
167
167
|
}
|
|
168
|
+
Function("editorInsertTableRow") { id: Int, after: Boolean ->
|
|
169
|
+
val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
|
|
170
|
+
editorInsertTableRow(editorId, after)
|
|
171
|
+
}
|
|
172
|
+
Function("editorDeleteTableRow") { id: Int ->
|
|
173
|
+
val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
|
|
174
|
+
editorDeleteTableRow(editorId)
|
|
175
|
+
}
|
|
176
|
+
Function("editorInsertTableColumn") { id: Int, after: Boolean ->
|
|
177
|
+
val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
|
|
178
|
+
editorInsertTableColumn(editorId, after)
|
|
179
|
+
}
|
|
180
|
+
Function("editorDeleteTableColumn") { id: Int ->
|
|
181
|
+
val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
|
|
182
|
+
editorDeleteTableColumn(editorId)
|
|
183
|
+
}
|
|
184
|
+
Function("editorMoveTableCell") { id: Int, forward: Boolean ->
|
|
185
|
+
val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
|
|
186
|
+
editorMoveTableCell(editorId, forward)
|
|
187
|
+
}
|
|
168
188
|
Function("editorInsertNode") { id: Int, nodeType: String ->
|
|
169
189
|
val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
|
|
170
190
|
editorInsertNode(editorId, nodeType)
|
|
@@ -359,6 +379,12 @@ class NativeEditorModule : Module() {
|
|
|
359
379
|
val headPosition = nativeUInt(head) ?: return@Function
|
|
360
380
|
editorSetSelection(editorId, anchorPosition, headPosition)
|
|
361
381
|
}
|
|
382
|
+
Function("editorSetCellSelection") { id: Int, anchor: Int, head: Int ->
|
|
383
|
+
val editorId = nativeULong(id) ?: return@Function
|
|
384
|
+
val anchorPosition = nativeUInt(anchor) ?: return@Function
|
|
385
|
+
val headPosition = nativeUInt(head) ?: return@Function
|
|
386
|
+
editorSetCellSelection(editorId, anchorPosition, headPosition)
|
|
387
|
+
}
|
|
362
388
|
Function("editorSetSelectionScalar") { id: Int, scalarAnchor: Int, scalarHead: Int ->
|
|
363
389
|
val editorId = nativeULong(id) ?: return@Function
|
|
364
390
|
val anchor = nativeUInt(scalarAnchor) ?: return@Function
|
|
@@ -405,33 +431,6 @@ class NativeEditorModule : Module() {
|
|
|
405
431
|
val editorId = nativeULong(id) ?: return@Function false
|
|
406
432
|
editorCanRedo(editorId)
|
|
407
433
|
}
|
|
408
|
-
Function("renderDocumentJson") { configJson: String, json: String ->
|
|
409
|
-
val editorId = editorCreate(configJson)
|
|
410
|
-
try {
|
|
411
|
-
editorSetJson(editorId, json)
|
|
412
|
-
} finally {
|
|
413
|
-
editorDestroy(editorId)
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
Function("measureContentHeight") { renderJson: String, themeJson: String?, width: Double ->
|
|
417
|
-
val density = appContext.reactContext?.resources?.displayMetrics?.density ?: 1f
|
|
418
|
-
val height = RenderBridge.measureHeight(
|
|
419
|
-
json = renderJson,
|
|
420
|
-
themeJson = themeJson,
|
|
421
|
-
width = width.toFloat(),
|
|
422
|
-
density = density
|
|
423
|
-
)
|
|
424
|
-
height.toDouble()
|
|
425
|
-
}
|
|
426
|
-
Function("renderDocumentHtml") { configJson: String, html: String ->
|
|
427
|
-
val editorId = editorCreate(configJson)
|
|
428
|
-
try {
|
|
429
|
-
editorSetHtml(editorId, html)
|
|
430
|
-
} finally {
|
|
431
|
-
editorDestroy(editorId)
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
434
|
View(NativeEditorExpoView::class) {
|
|
436
435
|
Events(
|
|
437
436
|
"onEditorUpdate",
|
|
@@ -534,28 +533,5 @@ class NativeEditorModule : Module() {
|
|
|
534
533
|
}
|
|
535
534
|
|
|
536
535
|
}
|
|
537
|
-
|
|
538
|
-
View(NativeProseViewerExpoView::class) {
|
|
539
|
-
Name("NativeProseViewer")
|
|
540
|
-
Events("onContentHeightChange", "onPressLink", "onPressMention")
|
|
541
|
-
|
|
542
|
-
Prop("renderJson") { view: NativeProseViewerExpoView, renderJson: String? ->
|
|
543
|
-
view.setRenderJson(renderJson)
|
|
544
|
-
}
|
|
545
|
-
Prop("themeJson") { view: NativeProseViewerExpoView, themeJson: String? ->
|
|
546
|
-
view.setThemeJson(themeJson)
|
|
547
|
-
}
|
|
548
|
-
Prop("collapsesWhenEmpty") {
|
|
549
|
-
view: NativeProseViewerExpoView,
|
|
550
|
-
collapsesWhenEmpty: Boolean? ->
|
|
551
|
-
view.setCollapsesWhenEmpty(collapsesWhenEmpty)
|
|
552
|
-
}
|
|
553
|
-
Prop("enableLinkTaps") { view: NativeProseViewerExpoView, enableLinkTaps: Boolean? ->
|
|
554
|
-
view.setEnableLinkTaps(enableLinkTaps)
|
|
555
|
-
}
|
|
556
|
-
Prop("interceptLinkTaps") { view: NativeProseViewerExpoView, interceptLinkTaps: Boolean? ->
|
|
557
|
-
view.setInterceptLinkTaps(interceptLinkTaps)
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
536
|
}
|
|
561
537
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package com.openeditor.editor
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.util.Base64
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import android.util.LruCache
|
|
8
|
+
import java.net.URL
|
|
9
|
+
import java.util.concurrent.ArrayBlockingQueue
|
|
10
|
+
import java.util.concurrent.ThreadPoolExecutor
|
|
11
|
+
import java.util.concurrent.TimeUnit
|
|
12
|
+
|
|
13
|
+
internal object NativeImageDecoder {
|
|
14
|
+
private const val MAX_DECODE_DIMENSION_PX = 2048
|
|
15
|
+
private const val LOG_TAG = "NativeEditorImage"
|
|
16
|
+
|
|
17
|
+
fun decodeSource(source: String): Bitmap? {
|
|
18
|
+
val bytes = decodeDataUrlBytes(source) ?: runCatching {
|
|
19
|
+
URL(source).openStream().use { it.readBytes() }
|
|
20
|
+
}.getOrNull() ?: return null
|
|
21
|
+
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
22
|
+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
|
|
23
|
+
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
|
|
24
|
+
val options = BitmapFactory.Options().apply {
|
|
25
|
+
inSampleSize = calculateInSampleSize(bounds.outWidth, bounds.outHeight)
|
|
26
|
+
}
|
|
27
|
+
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun decodeDataUrlBytes(source: String): ByteArray? {
|
|
31
|
+
val trimmed = source.trim()
|
|
32
|
+
if (!trimmed.startsWith("data:image/", ignoreCase = true)) return null
|
|
33
|
+
val comma = trimmed.indexOf(',')
|
|
34
|
+
if (comma <= 0 || !trimmed.substring(0, comma).contains(";base64", ignoreCase = true)) return null
|
|
35
|
+
val payload = trimmed.substring(comma + 1).filterNot(Char::isWhitespace)
|
|
36
|
+
for (flags in intArrayOf(Base64.DEFAULT, Base64.NO_WRAP, Base64.URL_SAFE or Base64.NO_WRAP)) {
|
|
37
|
+
runCatching { Base64.decode(payload, flags) }.getOrNull()?.let { return it }
|
|
38
|
+
}
|
|
39
|
+
Log.w(LOG_TAG, "Unsupported image data URL")
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun calculateInSampleSize(width: Int, height: Int): Int {
|
|
44
|
+
var sample = 1
|
|
45
|
+
while (width / sample > MAX_DECODE_DIMENSION_PX || height / sample > MAX_DECODE_DIMENSION_PX) {
|
|
46
|
+
sample *= 2
|
|
47
|
+
}
|
|
48
|
+
return sample
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
internal object NativeImageLoader {
|
|
53
|
+
private val cache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
|
|
54
|
+
override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
|
|
55
|
+
}
|
|
56
|
+
private val executor = ThreadPoolExecutor(
|
|
57
|
+
2,
|
|
58
|
+
2,
|
|
59
|
+
30,
|
|
60
|
+
TimeUnit.SECONDS,
|
|
61
|
+
ArrayBlockingQueue(64),
|
|
62
|
+
ThreadPoolExecutor.DiscardOldestPolicy()
|
|
63
|
+
)
|
|
64
|
+
private val lock = Any()
|
|
65
|
+
private val inFlight = mutableMapOf<String, MutableList<(Bitmap?) -> Unit>>()
|
|
66
|
+
|
|
67
|
+
@Volatile
|
|
68
|
+
internal var decodeSourceOverride: ((String) -> Bitmap?)? = null
|
|
69
|
+
|
|
70
|
+
internal fun resetForTesting() {
|
|
71
|
+
synchronized(cache) { cache.evictAll() }
|
|
72
|
+
synchronized(lock) { inFlight.clear() }
|
|
73
|
+
decodeSourceOverride = null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fun load(source: String, onLoaded: (Bitmap?) -> Unit) {
|
|
77
|
+
synchronized(cache) { cache.get(source) }?.let(onLoaded)?.also { return }
|
|
78
|
+
synchronized(lock) {
|
|
79
|
+
inFlight[source]?.let { it += onLoaded; return }
|
|
80
|
+
inFlight[source] = mutableListOf(onLoaded)
|
|
81
|
+
}
|
|
82
|
+
executor.execute {
|
|
83
|
+
val bitmap = decodeSourceOverride?.invoke(source) ?: NativeImageDecoder.decodeSource(source)
|
|
84
|
+
if (bitmap != null) synchronized(cache) { cache.put(source, bitmap) }
|
|
85
|
+
val callbacks = synchronized(lock) { inFlight.remove(source).orEmpty() }
|
|
86
|
+
callbacks.forEach { it(bitmap) }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -11,7 +11,6 @@ import android.util.TypedValue
|
|
|
11
11
|
import android.view.View
|
|
12
12
|
import androidx.appcompat.content.res.AppCompatResources
|
|
13
13
|
import org.json.JSONArray
|
|
14
|
-
import uniffi.editor_core.editorDocToScalar
|
|
15
14
|
|
|
16
15
|
data class RemoteSelectionDecoration(
|
|
17
16
|
val clientId: Int,
|
|
@@ -102,11 +101,9 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
102
101
|
|
|
103
102
|
private data class GeometrySnapshot(
|
|
104
103
|
val editorId: Long,
|
|
105
|
-
val
|
|
106
|
-
val
|
|
107
|
-
val
|
|
108
|
-
val baseX: Int,
|
|
109
|
-
val baseY: Int,
|
|
104
|
+
val surfaceRevision: Long,
|
|
105
|
+
val surfaceWidth: Int,
|
|
106
|
+
val surfaceHeight: Int,
|
|
110
107
|
val width: Int,
|
|
111
108
|
val height: Int,
|
|
112
109
|
val selections: List<RemoteSelectionDecoration>,
|
|
@@ -114,8 +111,9 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
114
111
|
|
|
115
112
|
private data class GeometryContext(
|
|
116
113
|
val snapshot: GeometrySnapshot,
|
|
117
|
-
val
|
|
118
|
-
val
|
|
114
|
+
val surface: NativeBlockEditorSurface,
|
|
115
|
+
val baseX: Float,
|
|
116
|
+
val baseY: Float,
|
|
119
117
|
)
|
|
120
118
|
|
|
121
119
|
private var editorView: RichTextEditorView? = null
|
|
@@ -123,9 +121,6 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
123
121
|
private var cachedSnapshot: GeometrySnapshot? = null
|
|
124
122
|
private var cachedGeometry: List<CachedSelectionGeometry> = emptyList()
|
|
125
123
|
internal var editorIdOverrideForTesting: Long? = null
|
|
126
|
-
internal var docToScalarResolver: (Long, Int) -> Int = { editorId, docPos ->
|
|
127
|
-
editorDocToScalar(editorId.toULong(), docPos.toUInt()).toInt()
|
|
128
|
-
}
|
|
129
124
|
private val selectionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
130
125
|
style = Paint.Style.FILL
|
|
131
126
|
}
|
|
@@ -208,20 +203,21 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
208
203
|
return cachedGeometry
|
|
209
204
|
}
|
|
210
205
|
|
|
211
|
-
val text = context.snapshot.text
|
|
212
|
-
val editorId = context.snapshot.editorId
|
|
213
206
|
val geometry = remoteSelections.map { selection ->
|
|
214
207
|
val startDoc = minOf(selection.anchor, selection.head)
|
|
215
208
|
val endDoc = maxOf(selection.anchor, selection.head)
|
|
216
|
-
val
|
|
217
|
-
val
|
|
218
|
-
val startUtf16 = PositionBridge.scalarToUtf16(startScalar, text).coerceIn(0, text.length)
|
|
219
|
-
val endUtf16 = PositionBridge.scalarToUtf16(endScalar, text).coerceIn(0, text.length)
|
|
220
|
-
|
|
221
|
-
val selectionPath = if (startUtf16 != endUtf16) {
|
|
209
|
+
val selectionRects = context.surface.selectionRectsForDocRange(startDoc, endDoc)
|
|
210
|
+
val selectionPath = if (selectionRects.isNotEmpty()) {
|
|
222
211
|
Path().apply {
|
|
223
|
-
|
|
224
|
-
|
|
212
|
+
selectionRects.forEach { rect ->
|
|
213
|
+
addRect(
|
|
214
|
+
rect.left + context.baseX,
|
|
215
|
+
rect.top + context.baseY,
|
|
216
|
+
rect.right + context.baseX,
|
|
217
|
+
rect.bottom + context.baseY,
|
|
218
|
+
Path.Direction.CW
|
|
219
|
+
)
|
|
220
|
+
}
|
|
225
221
|
}
|
|
226
222
|
} else {
|
|
227
223
|
null
|
|
@@ -231,15 +227,11 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
231
227
|
clientId = selection.clientId,
|
|
232
228
|
selectionPath = selectionPath,
|
|
233
229
|
selectionColor = withAlpha(selection.color, 0.18f),
|
|
234
|
-
caretRect =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
baseY = context.snapshot.baseY.toFloat(),
|
|
240
|
-
caretWidth = context.caretWidth,
|
|
241
|
-
isFocused = selection.isFocused,
|
|
242
|
-
),
|
|
230
|
+
caretRect = if (selection.isFocused) {
|
|
231
|
+
context.surface.caretRectForDocPosition(selection.head)?.apply {
|
|
232
|
+
offset(context.baseX, context.baseY)
|
|
233
|
+
}
|
|
234
|
+
} else null,
|
|
243
235
|
caretColor = selection.color,
|
|
244
236
|
)
|
|
245
237
|
}
|
|
@@ -254,29 +246,23 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
254
246
|
val editorId = resolvedEditorId(editorView)
|
|
255
247
|
if (editorId == 0L || remoteSelections.isEmpty()) return null
|
|
256
248
|
|
|
257
|
-
val
|
|
258
|
-
val
|
|
259
|
-
val
|
|
260
|
-
val baseX = editorView.editorViewport.left + editorView.editorScrollView.left + editText.left +
|
|
261
|
-
editText.compoundPaddingLeft
|
|
262
|
-
val baseY = editorView.editorViewport.top + editorView.editorScrollView.top + editText.top +
|
|
263
|
-
editText.compoundPaddingTop - editorView.editorScrollView.scrollY
|
|
264
|
-
val caretWidth = maxOf(2f, resources.displayMetrics.density)
|
|
249
|
+
val surface = editorView.blockSurface
|
|
250
|
+
val baseX = (editorView.editorViewport.left + surface.left).toFloat()
|
|
251
|
+
val baseY = (editorView.editorViewport.top + surface.top).toFloat()
|
|
265
252
|
|
|
266
253
|
return GeometryContext(
|
|
267
254
|
snapshot = GeometrySnapshot(
|
|
268
255
|
editorId = editorId,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
baseX = baseX,
|
|
273
|
-
baseY = baseY,
|
|
256
|
+
surfaceRevision = surface.geometryRevision,
|
|
257
|
+
surfaceWidth = surface.width,
|
|
258
|
+
surfaceHeight = surface.height,
|
|
274
259
|
width = width,
|
|
275
260
|
height = height,
|
|
276
261
|
selections = remoteSelections,
|
|
277
262
|
),
|
|
278
|
-
|
|
279
|
-
|
|
263
|
+
surface = surface,
|
|
264
|
+
baseX = baseX,
|
|
265
|
+
baseY = baseY,
|
|
280
266
|
)
|
|
281
267
|
}
|
|
282
268
|
|
|
@@ -284,27 +270,6 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
284
270
|
cachedSnapshot = null
|
|
285
271
|
}
|
|
286
272
|
|
|
287
|
-
private fun caretRectForOffset(
|
|
288
|
-
endUtf16: Int,
|
|
289
|
-
textLength: Int,
|
|
290
|
-
layout: android.text.Layout,
|
|
291
|
-
baseX: Float,
|
|
292
|
-
baseY: Float,
|
|
293
|
-
caretWidth: Float,
|
|
294
|
-
isFocused: Boolean,
|
|
295
|
-
): RectF? {
|
|
296
|
-
if (!isFocused) return null
|
|
297
|
-
|
|
298
|
-
val clampedOffset = endUtf16.coerceIn(0, textLength)
|
|
299
|
-
val lineLookupOffset = clampedOffset.coerceAtMost(maxOf(textLength - 1, 0))
|
|
300
|
-
val line = layout.getLineForOffset(lineLookupOffset)
|
|
301
|
-
val horizontal = layout.getPrimaryHorizontal(clampedOffset)
|
|
302
|
-
val caretLeft = baseX + horizontal
|
|
303
|
-
val caretTop = baseY + layout.getLineTop(line)
|
|
304
|
-
val caretBottom = baseY + layout.getLineBottom(line)
|
|
305
|
-
return RectF(caretLeft, caretTop, caretLeft + caretWidth, caretBottom)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
273
|
private fun withAlpha(color: Int, alphaFraction: Float): Int {
|
|
309
274
|
val alpha = (255f * alphaFraction).toInt().coerceIn(0, 255)
|
|
310
275
|
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
|
|
@@ -7,6 +7,7 @@ import android.graphics.drawable.GradientDrawable
|
|
|
7
7
|
import android.util.AttributeSet
|
|
8
8
|
import android.view.MotionEvent
|
|
9
9
|
import android.view.ViewGroup
|
|
10
|
+
import android.view.inputmethod.InputMethodManager
|
|
10
11
|
import android.widget.FrameLayout
|
|
11
12
|
import android.widget.LinearLayout
|
|
12
13
|
import android.widget.ScrollView
|
|
@@ -20,7 +21,7 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
20
21
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
|
21
22
|
val editorViewport: FrameLayout
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
class EditorScrollView(context: Context) : ScrollView(context) {
|
|
24
25
|
private fun updateParentIntercept(action: Int) {
|
|
25
26
|
val canScroll = canScrollVertically(-1) || canScrollVertically(1)
|
|
26
27
|
if (!canScroll) return
|
|
@@ -44,7 +45,8 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
val editorEditText: EditorEditText
|
|
47
|
-
val editorScrollView:
|
|
48
|
+
val editorScrollView: EditorScrollView
|
|
49
|
+
val blockSurface: NativeBlockEditorSurface
|
|
48
50
|
private val remoteSelectionOverlayView: RemoteSelectionOverlayView
|
|
49
51
|
private val imageResizeOverlayView: ImageResizeOverlayView
|
|
50
52
|
|
|
@@ -58,6 +60,7 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
58
60
|
|
|
59
61
|
private var currentEditorId: Long = 0
|
|
60
62
|
private var deferEditorUnbindOnDetach = false
|
|
63
|
+
private var isSyncingBlockSurfaceUpdateToInputAdapter = false
|
|
61
64
|
internal var onBeforeDetachedFromWindow: (() -> Unit)? = null
|
|
62
65
|
|
|
63
66
|
/** Binds or unbinds the Rust editor instance. */
|
|
@@ -74,7 +77,9 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
74
77
|
editorScrollView = EditorScrollView(context).apply {
|
|
75
78
|
clipToPadding = false
|
|
76
79
|
isFillViewport = false
|
|
80
|
+
alpha = 0.01f
|
|
77
81
|
}
|
|
82
|
+
blockSurface = NativeBlockEditorSurface(context)
|
|
78
83
|
editorViewport = FrameLayout(context)
|
|
79
84
|
remoteSelectionOverlayView = RemoteSelectionOverlayView(context)
|
|
80
85
|
imageResizeOverlayView = ImageResizeOverlayView(context)
|
|
@@ -86,6 +91,13 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
86
91
|
ViewGroup.LayoutParams.MATCH_PARENT
|
|
87
92
|
)
|
|
88
93
|
)
|
|
94
|
+
editorViewport.addView(
|
|
95
|
+
blockSurface,
|
|
96
|
+
FrameLayout.LayoutParams(
|
|
97
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
98
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
99
|
+
)
|
|
100
|
+
)
|
|
89
101
|
editorViewport.addView(
|
|
90
102
|
remoteSelectionOverlayView,
|
|
91
103
|
FrameLayout.LayoutParams(
|
|
@@ -105,7 +117,43 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
105
117
|
editorScrollView.setOnScrollChangeListener { _, _, _, _, _ ->
|
|
106
118
|
refreshOverlays()
|
|
107
119
|
}
|
|
108
|
-
editorEditText.onSelectionOrContentMayChange = {
|
|
120
|
+
editorEditText.onSelectionOrContentMayChange = {
|
|
121
|
+
if (editorId != 0L && !isSyncingBlockSurfaceUpdateToInputAdapter) {
|
|
122
|
+
blockSurface.applyUpdateJSON(
|
|
123
|
+
editorGetSelectionState(editorId.toULong()),
|
|
124
|
+
editorId
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
refreshOverlays()
|
|
128
|
+
}
|
|
129
|
+
editorEditText.onUpdateJSONApplied = appliedUpdate@ { updateJSON ->
|
|
130
|
+
if (isSyncingBlockSurfaceUpdateToInputAdapter) return@appliedUpdate
|
|
131
|
+
blockSurface.applyUpdateJSON(updateJSON, editorId)
|
|
132
|
+
}
|
|
133
|
+
blockSurface.onAppliedUpdate = { updateJSON ->
|
|
134
|
+
isSyncingBlockSurfaceUpdateToInputAdapter = true
|
|
135
|
+
try {
|
|
136
|
+
editorEditText.applyUpdateJSON(updateJSON, notifyListener = true)
|
|
137
|
+
} finally {
|
|
138
|
+
isSyncingBlockSurfaceUpdateToInputAdapter = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
blockSurface.onRequestTextInput = requestTextInput@ { docPos ->
|
|
142
|
+
if (editorId == 0L || !editorEditText.isEditable) return@requestTextInput
|
|
143
|
+
editorSetSelection(editorId.toULong(), docPos.toUInt(), docPos.toUInt())
|
|
144
|
+
isSyncingBlockSurfaceUpdateToInputAdapter = true
|
|
145
|
+
try {
|
|
146
|
+
editorEditText.applyUpdateJSON(
|
|
147
|
+
editorGetSelectionState(editorId.toULong()),
|
|
148
|
+
notifyListener = true
|
|
149
|
+
)
|
|
150
|
+
} finally {
|
|
151
|
+
isSyncingBlockSurfaceUpdateToInputAdapter = false
|
|
152
|
+
}
|
|
153
|
+
editorEditText.requestFocus()
|
|
154
|
+
context.getSystemService(InputMethodManager::class.java)
|
|
155
|
+
?.showSoftInput(editorEditText, InputMethodManager.SHOW_IMPLICIT)
|
|
156
|
+
}
|
|
109
157
|
|
|
110
158
|
addView(editorViewport, createContainerLayoutParams())
|
|
111
159
|
updateScrollContainerAppearance()
|
|
@@ -119,6 +167,7 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
119
167
|
) {
|
|
120
168
|
baseBackgroundColor = backgroundColor
|
|
121
169
|
editorEditText.setBaseStyle(textSizePx, textColor, backgroundColor)
|
|
170
|
+
blockSurface.configure(textSizePx, textColor, backgroundColor)
|
|
122
171
|
updateScrollContainerAppearance()
|
|
123
172
|
refreshOverlays()
|
|
124
173
|
}
|
|
@@ -127,6 +176,7 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
127
176
|
this.theme = theme
|
|
128
177
|
val previousScrollY = editorScrollView.scrollY
|
|
129
178
|
editorEditText.applyTheme(theme)
|
|
179
|
+
blockSurface.applyTheme(theme)
|
|
130
180
|
updateScrollContainerAppearance()
|
|
131
181
|
updateScrollContainerInsets()
|
|
132
182
|
if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
@@ -143,6 +193,12 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
143
193
|
refreshOverlays()
|
|
144
194
|
}
|
|
145
195
|
|
|
196
|
+
fun setEditable(editable: Boolean) {
|
|
197
|
+
editorEditText.isEditable = editable
|
|
198
|
+
blockSurface.isEditable = editable
|
|
199
|
+
if (!editable) editorEditText.clearFocus()
|
|
200
|
+
}
|
|
201
|
+
|
|
146
202
|
fun setHeightBehavior(heightBehavior: EditorHeightBehavior) {
|
|
147
203
|
if (this.heightBehavior == heightBehavior) return
|
|
148
204
|
this.heightBehavior = heightBehavior
|
|
@@ -155,6 +211,13 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
155
211
|
} else {
|
|
156
212
|
OVER_SCROLL_NEVER
|
|
157
213
|
}
|
|
214
|
+
blockSurface.isScrollingEnabled = heightBehavior == EditorHeightBehavior.FIXED
|
|
215
|
+
blockSurface.isVerticalScrollBarEnabled = heightBehavior == EditorHeightBehavior.FIXED
|
|
216
|
+
blockSurface.overScrollMode = if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
217
|
+
OVER_SCROLL_IF_CONTENT_SCROLLS
|
|
218
|
+
} else {
|
|
219
|
+
OVER_SCROLL_NEVER
|
|
220
|
+
}
|
|
158
221
|
updateScrollContainerInsets()
|
|
159
222
|
refreshOverlays()
|
|
160
223
|
requestLayout()
|
|
@@ -198,10 +261,6 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
198
261
|
fun remoteSelectionDebugSnapshotsForTesting(): List<RemoteSelectionDebugSnapshot> =
|
|
199
262
|
remoteSelectionOverlayView.debugSnapshotsForTesting()
|
|
200
263
|
|
|
201
|
-
fun setRemoteSelectionScalarResolverForTesting(resolver: (Long, Int) -> Int) {
|
|
202
|
-
remoteSelectionOverlayView.docToScalarResolver = resolver
|
|
203
|
-
}
|
|
204
|
-
|
|
205
264
|
fun setRemoteSelectionEditorIdForTesting(editorId: Long) {
|
|
206
265
|
remoteSelectionOverlayView.editorIdOverrideForTesting = editorId
|
|
207
266
|
}
|
|
@@ -209,21 +268,27 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
209
268
|
fun setContent(html: String) {
|
|
210
269
|
if (editorId == 0L) return
|
|
211
270
|
editorSetHtml(editorId.toULong(), html)
|
|
212
|
-
|
|
213
|
-
editorGetCurrentState(editorId.toULong()),
|
|
214
|
-
notifyListener = false,
|
|
215
|
-
refreshInputConnectionForExternalUpdate = true
|
|
216
|
-
)
|
|
271
|
+
applyFullStateToInputAndSurface(editorGetCurrentState(editorId.toULong()))
|
|
217
272
|
}
|
|
218
273
|
|
|
219
274
|
fun setContent(json: org.json.JSONObject) {
|
|
220
275
|
if (editorId == 0L) return
|
|
221
276
|
editorSetJson(editorId.toULong(), json.toString())
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
277
|
+
applyFullStateToInputAndSurface(editorGetCurrentState(editorId.toULong()))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private fun applyFullStateToInputAndSurface(stateJSON: String) {
|
|
281
|
+
isSyncingBlockSurfaceUpdateToInputAdapter = true
|
|
282
|
+
try {
|
|
283
|
+
editorEditText.applyUpdateJSON(
|
|
284
|
+
stateJSON,
|
|
285
|
+
notifyListener = false,
|
|
286
|
+
refreshInputConnectionForExternalUpdate = true
|
|
287
|
+
)
|
|
288
|
+
} finally {
|
|
289
|
+
isSyncingBlockSurfaceUpdateToInputAdapter = false
|
|
290
|
+
}
|
|
291
|
+
blockSurface.applyUpdateJSON(stateJSON, editorId)
|
|
227
292
|
}
|
|
228
293
|
|
|
229
294
|
internal fun rebindEditorIfNeeded(notifyListener: Boolean = true) {
|
|
@@ -259,6 +324,7 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
259
324
|
editorEditText.discardTransientNativeInputForEditorRebind()
|
|
260
325
|
}
|
|
261
326
|
currentEditorId = value
|
|
327
|
+
blockSurface.bind(value)
|
|
262
328
|
if (bindEditor && value != 0L) {
|
|
263
329
|
editorEditText.bindEditor(value, notifyListener = notifyListener)
|
|
264
330
|
} else {
|
|
@@ -346,34 +412,32 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
346
412
|
)
|
|
347
413
|
|
|
348
414
|
internal fun selectedImageGeometry(): EditorEditText.SelectedImageGeometry? {
|
|
349
|
-
|
|
415
|
+
if (!imageResizingEnabled) return null
|
|
416
|
+
val geometry = blockSurface.selectedImageGeometry() ?: return null
|
|
350
417
|
return EditorEditText.SelectedImageGeometry(
|
|
351
418
|
docPos = geometry.docPos,
|
|
352
419
|
rect = RectF(
|
|
353
|
-
editorViewport.left +
|
|
354
|
-
editorViewport.top +
|
|
355
|
-
editorViewport.left +
|
|
356
|
-
editorViewport.top +
|
|
420
|
+
editorViewport.left + blockSurface.left + geometry.rect.left,
|
|
421
|
+
editorViewport.top + blockSurface.top + geometry.rect.top,
|
|
422
|
+
editorViewport.left + blockSurface.left + geometry.rect.right,
|
|
423
|
+
editorViewport.top + blockSurface.top + geometry.rect.bottom
|
|
357
424
|
)
|
|
358
425
|
)
|
|
359
426
|
}
|
|
360
427
|
|
|
361
428
|
internal fun caretRect(): RectF? {
|
|
362
|
-
|
|
429
|
+
blockSurface.isInputFocused = editorEditText.hasFocus()
|
|
430
|
+
val rect = blockSurface.selectedCaretRect() ?: return null
|
|
363
431
|
return RectF(
|
|
364
|
-
editorViewport.left +
|
|
365
|
-
editorViewport.top +
|
|
366
|
-
editorViewport.left +
|
|
367
|
-
editorViewport.top +
|
|
432
|
+
editorViewport.left + blockSurface.left + rect.left,
|
|
433
|
+
editorViewport.top + blockSurface.top + rect.top,
|
|
434
|
+
editorViewport.left + blockSurface.left + rect.right,
|
|
435
|
+
editorViewport.top + blockSurface.top + rect.bottom
|
|
368
436
|
)
|
|
369
437
|
}
|
|
370
438
|
|
|
371
439
|
internal fun maximumImageWidthPx(): Float {
|
|
372
|
-
|
|
373
|
-
maxOf(editorEditText.width, editorEditText.measuredWidth) -
|
|
374
|
-
editorEditText.compoundPaddingLeft -
|
|
375
|
-
editorEditText.compoundPaddingRight
|
|
376
|
-
return availableWidth.coerceAtLeast(48).toFloat()
|
|
440
|
+
return blockSurface.maximumImageWidthPx()
|
|
377
441
|
}
|
|
378
442
|
|
|
379
443
|
internal fun clampImageSize(
|
|
@@ -389,10 +453,11 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
389
453
|
|
|
390
454
|
internal fun resizeImage(docPos: Int, widthPx: Float, heightPx: Float) {
|
|
391
455
|
val (clampedWidth, clampedHeight) = clampImageSize(widthPx, heightPx)
|
|
392
|
-
|
|
456
|
+
blockSurface.resizeImage(docPos, clampedWidth, clampedHeight)
|
|
393
457
|
}
|
|
394
458
|
|
|
395
459
|
private fun refreshOverlays() {
|
|
460
|
+
blockSurface.isInputFocused = editorEditText.hasFocus()
|
|
396
461
|
remoteSelectionOverlayView.refreshGeometry()
|
|
397
462
|
imageResizeOverlayView.refresh()
|
|
398
463
|
}
|
package/dist/EditorToolbar.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { EditorMentionTheme, EditorToolbarTheme } from './EditorTheme';
|
|
|
4
4
|
import type { MentionSuggestion } from './addons';
|
|
5
5
|
export type EditorToolbarListType = 'bulletList' | 'orderedList';
|
|
6
6
|
export type EditorToolbarHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
7
|
-
export type EditorToolbarCommand = 'indentList' | 'outdentList' | 'undo' | 'redo';
|
|
7
|
+
export type EditorToolbarCommand = 'indentList' | 'outdentList' | 'insertTableRowBefore' | 'insertTableRowAfter' | 'deleteTableRow' | 'insertTableColumnBefore' | 'insertTableColumnAfter' | 'deleteTableColumn' | 'moveToNextTableCell' | 'moveToPreviousTableCell' | 'undo' | 'redo';
|
|
8
8
|
export type EditorToolbarGroupPresentation = 'expand' | 'menu';
|
|
9
9
|
export type EditorToolbarItemPlacement = 'start' | 'scroll' | 'end';
|
|
10
10
|
export type EditorToolbarDefaultIconId = 'bold' | 'italic' | 'underline' | 'strike' | 'link' | 'image' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote' | 'bulletList' | 'orderedList' | 'indentList' | 'outdentList' | 'lineBreak' | 'horizontalRule' | 'undo' | 'redo';
|
package/dist/EditorToolbar.js
CHANGED
|
@@ -342,6 +342,15 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
342
342
|
return onIndentList ?? null;
|
|
343
343
|
case 'outdentList':
|
|
344
344
|
return onOutdentList ?? null;
|
|
345
|
+
case 'insertTableRowBefore':
|
|
346
|
+
case 'insertTableRowAfter':
|
|
347
|
+
case 'deleteTableRow':
|
|
348
|
+
case 'insertTableColumnBefore':
|
|
349
|
+
case 'insertTableColumnAfter':
|
|
350
|
+
case 'deleteTableColumn':
|
|
351
|
+
case 'moveToNextTableCell':
|
|
352
|
+
case 'moveToPreviousTableCell':
|
|
353
|
+
return null;
|
|
345
354
|
case 'undo':
|
|
346
355
|
return onUndo;
|
|
347
356
|
case 'redo':
|
|
@@ -434,6 +443,16 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
434
443
|
case 'outdentList':
|
|
435
444
|
isDisabled = !canOutdentList;
|
|
436
445
|
break;
|
|
446
|
+
case 'insertTableRowBefore':
|
|
447
|
+
case 'insertTableRowAfter':
|
|
448
|
+
case 'deleteTableRow':
|
|
449
|
+
case 'insertTableColumnBefore':
|
|
450
|
+
case 'insertTableColumnAfter':
|
|
451
|
+
case 'deleteTableColumn':
|
|
452
|
+
case 'moveToNextTableCell':
|
|
453
|
+
case 'moveToPreviousTableCell':
|
|
454
|
+
isDisabled = !commands[item.command];
|
|
455
|
+
break;
|
|
437
456
|
case 'undo':
|
|
438
457
|
isDisabled = !historyState.canUndo;
|
|
439
458
|
break;
|