@openeditor/react-native-prose-editor 0.0.8 → 0.0.10
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 +5 -9
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +381 -49
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +110 -169
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +124 -337
- package/dist/EditorTheme.d.ts +8 -0
- package/ios/NativeBlockEditorSurface.swift +323 -61
- package/ios/NativeEditorExpoView.swift +114 -117
- package/ios/NativeEditorModule.swift +1 -1
- package/ios/NativeInputSupport.swift +0 -302
- package/ios/PositionBridge.swift +22 -548
- package/ios/RichTextEditorView.swift +229 -4664
- package/package.json +1 -1
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +0 -50
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +0 -4068
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +0 -1009
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +0 -6
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
package com.openeditor.editor
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
-
import android.graphics.Canvas
|
|
5
4
|
import android.graphics.Color
|
|
6
|
-
import android.graphics.Paint
|
|
7
5
|
import android.graphics.Rect
|
|
8
6
|
import android.graphics.RectF
|
|
9
7
|
import android.graphics.Typeface
|
|
10
8
|
import android.graphics.drawable.GradientDrawable
|
|
11
9
|
import android.text.SpannableStringBuilder
|
|
12
10
|
import android.text.Spanned
|
|
11
|
+
import android.text.Editable
|
|
12
|
+
import android.text.TextWatcher
|
|
13
13
|
import android.text.style.BackgroundColorSpan
|
|
14
14
|
import android.text.style.ForegroundColorSpan
|
|
15
15
|
import android.text.style.StrikethroughSpan
|
|
@@ -19,20 +19,32 @@ import android.text.style.UnderlineSpan
|
|
|
19
19
|
import android.view.MotionEvent
|
|
20
20
|
import android.view.View
|
|
21
21
|
import android.view.ViewGroup
|
|
22
|
+
import android.view.inputmethod.BaseInputConnection
|
|
23
|
+
import android.view.inputmethod.EditorInfo
|
|
24
|
+
import android.view.inputmethod.InputConnection
|
|
25
|
+
import android.view.inputmethod.InputConnectionWrapper
|
|
26
|
+
import android.view.inputmethod.InputMethodManager
|
|
22
27
|
import android.util.TypedValue
|
|
28
|
+
import android.text.InputType
|
|
23
29
|
import android.widget.ImageView
|
|
24
30
|
import android.widget.LinearLayout
|
|
25
31
|
import android.widget.ScrollView
|
|
26
32
|
import android.widget.TextView
|
|
33
|
+
import androidx.appcompat.widget.AppCompatEditText
|
|
27
34
|
import org.json.JSONObject
|
|
28
35
|
import uniffi.editor_core.editorDocToScalar
|
|
36
|
+
import uniffi.editor_core.editorDeleteBackwardAtSelectionScalar
|
|
29
37
|
import uniffi.editor_core.editorGetSelectionState
|
|
38
|
+
import uniffi.editor_core.editorReplaceSelectionText
|
|
30
39
|
import uniffi.editor_core.editorResizeImageAtDocPos
|
|
31
40
|
import uniffi.editor_core.editorSetCellSelection
|
|
32
41
|
import uniffi.editor_core.editorSetNodeSelection
|
|
33
42
|
import uniffi.editor_core.editorSetSelection
|
|
43
|
+
import uniffi.editor_core.editorSplitBlock
|
|
34
44
|
import uniffi.editor_core.editorToggleTaskItemCheckedAtSelectionScalar
|
|
35
45
|
|
|
46
|
+
internal data class SelectedImageGeometry(val docPos: Int, val rect: RectF)
|
|
47
|
+
|
|
36
48
|
class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
37
49
|
var isScrollingEnabled: Boolean = true
|
|
38
50
|
|
|
@@ -48,8 +60,61 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
48
60
|
val isAtom: Boolean
|
|
49
61
|
)
|
|
50
62
|
|
|
51
|
-
private class TextLeaf(context: Context) :
|
|
63
|
+
private class TextLeaf(context: Context) : AppCompatEditText(context) {
|
|
52
64
|
var positionSegments: List<PositionSegment> = emptyList()
|
|
65
|
+
var editorSurface: NativeBlockEditorSurface? = null
|
|
66
|
+
private var beforeText: String = ""
|
|
67
|
+
private var compositionBaselineText: String? = null
|
|
68
|
+
private var beforeSelectionStart: Int = 0
|
|
69
|
+
private var beforeSelectionEnd: Int = 0
|
|
70
|
+
|
|
71
|
+
init {
|
|
72
|
+
addTextChangedListener(object : TextWatcher {
|
|
73
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
|
74
|
+
beforeText = s?.toString().orEmpty()
|
|
75
|
+
if (editorSurface?.isApplyingRemoteUpdate != true && compositionBaselineText == null) {
|
|
76
|
+
compositionBaselineText = beforeText
|
|
77
|
+
}
|
|
78
|
+
beforeSelectionStart = selectionStart.coerceAtLeast(0)
|
|
79
|
+
beforeSelectionEnd = selectionEnd.coerceAtLeast(beforeSelectionStart)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
|
83
|
+
|
|
84
|
+
override fun afterTextChanged(s: Editable?) {
|
|
85
|
+
val surface = editorSurface ?: return
|
|
86
|
+
if (surface.isApplyingRemoteUpdate) return
|
|
87
|
+
if (s != null && BaseInputConnection.getComposingSpanStart(s) >= 0) return
|
|
88
|
+
val baseline = compositionBaselineText ?: beforeText
|
|
89
|
+
compositionBaselineText = null
|
|
90
|
+
surface.commitLeafTextMutation(this@TextLeaf, baseline, s?.toString().orEmpty())
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
|
96
|
+
super.onSelectionChanged(selStart, selEnd)
|
|
97
|
+
editorSurface?.selectionChanged(this, selStart, selEnd)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
|
101
|
+
val target = super.onCreateInputConnection(outAttrs) ?: return null
|
|
102
|
+
return object : InputConnectionWrapper(target, false) {
|
|
103
|
+
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
|
|
104
|
+
if (beforeLength > 0 && selectionStart == 0 && selectionEnd == 0 &&
|
|
105
|
+
editorSurface?.deleteBackwardAtLeafBoundary(this@TextLeaf) == true
|
|
106
|
+
) return true
|
|
107
|
+
return super.deleteSurroundingText(beforeLength, afterLength)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
|
|
111
|
+
if (beforeLength > 0 && selectionStart == 0 && selectionEnd == 0 &&
|
|
112
|
+
editorSurface?.deleteBackwardAtLeafBoundary(this@TextLeaf) == true
|
|
113
|
+
) return true
|
|
114
|
+
return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
53
118
|
|
|
54
119
|
fun docPositionForOffset(offset: Int): Int? {
|
|
55
120
|
val segment = positionSegments.firstOrNull {
|
|
@@ -82,8 +147,9 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
82
147
|
|
|
83
148
|
fun caretRectForDocPosition(docPosition: Int, caretWidth: Float): RectF? {
|
|
84
149
|
val textLayout = layout ?: return null
|
|
85
|
-
val
|
|
86
|
-
val
|
|
150
|
+
val textLength = text?.length ?: 0
|
|
151
|
+
val offset = nativeOffsetForDocPosition(docPosition)?.coerceIn(0, textLength) ?: return null
|
|
152
|
+
val lookupOffset = offset.coerceAtMost((textLength - 1).coerceAtLeast(0))
|
|
87
153
|
val line = textLayout.getLineForOffset(lookupOffset)
|
|
88
154
|
val left = compoundPaddingLeft + textLayout.getPrimaryHorizontal(offset)
|
|
89
155
|
return RectF(
|
|
@@ -96,8 +162,9 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
96
162
|
|
|
97
163
|
fun selectionRectsForDocRange(from: Int, to: Int): List<RectF> {
|
|
98
164
|
val textLayout = layout ?: return emptyList()
|
|
99
|
-
val
|
|
100
|
-
val
|
|
165
|
+
val textLength = text?.length ?: 0
|
|
166
|
+
val start = nativeOffsetForDocPosition(from)?.coerceIn(0, textLength) ?: return emptyList()
|
|
167
|
+
val end = nativeOffsetForDocPosition(to)?.coerceIn(start, textLength) ?: return emptyList()
|
|
101
168
|
if (start == end) return emptyList()
|
|
102
169
|
val firstLine = textLayout.getLineForOffset(start)
|
|
103
170
|
val lastLine = textLayout.getLineForOffset((end - 1).coerceAtLeast(start))
|
|
@@ -162,22 +229,117 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
162
229
|
private val textLeafViews = mutableListOf<TextLeaf>()
|
|
163
230
|
private var selectedNodePos: Int? = null
|
|
164
231
|
private var selectedTextPos: Int? = null
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
var
|
|
232
|
+
var onAppliedUpdate: ((String) -> Unit)? = null
|
|
233
|
+
var onSelectionChange: ((Int, Int) -> Unit)? = null
|
|
234
|
+
var onEditorUpdate: ((String) -> Unit)? = null
|
|
235
|
+
var onFocusChange: ((Boolean) -> Unit)? = null
|
|
236
|
+
var placeholder: String = ""
|
|
168
237
|
set(value) {
|
|
169
238
|
field = value
|
|
170
|
-
|
|
239
|
+
textLeafViews.forEach { updateLeafHint(it) }
|
|
171
240
|
}
|
|
172
|
-
|
|
173
|
-
var
|
|
174
|
-
var
|
|
241
|
+
private var configuredAutoCapitalize: String? = null
|
|
242
|
+
private var configuredAutoCorrect: Boolean? = null
|
|
243
|
+
private var configuredKeyboardType: String? = null
|
|
175
244
|
var isEditable: Boolean = true
|
|
176
245
|
set(value) {
|
|
177
246
|
field = value
|
|
247
|
+
textLeafViews.forEach {
|
|
248
|
+
it.isEnabled = value
|
|
249
|
+
it.isFocusable = value
|
|
250
|
+
it.isFocusableInTouchMode = value
|
|
251
|
+
it.setTextIsSelectable(value)
|
|
252
|
+
}
|
|
178
253
|
refreshImageSelection()
|
|
179
254
|
}
|
|
180
255
|
|
|
256
|
+
internal val activeEditText: AppCompatEditText?
|
|
257
|
+
get() = textLeafViews.firstOrNull { it.hasFocus() }
|
|
258
|
+
?: selectedTextPos?.let { position ->
|
|
259
|
+
textLeafViews.firstOrNull { it.nativeOffsetForDocPosition(position) != null }
|
|
260
|
+
}
|
|
261
|
+
?: textLeafViews.firstOrNull()
|
|
262
|
+
|
|
263
|
+
val isEditorFocused: Boolean get() = textLeafViews.any { it.hasFocus() }
|
|
264
|
+
|
|
265
|
+
fun focus(): Boolean {
|
|
266
|
+
val leaf = activeEditText ?: return false
|
|
267
|
+
val focused = leaf.requestFocus()
|
|
268
|
+
if (focused) {
|
|
269
|
+
context.getSystemService(InputMethodManager::class.java)
|
|
270
|
+
?.showSoftInput(leaf, InputMethodManager.SHOW_IMPLICIT)
|
|
271
|
+
}
|
|
272
|
+
return focused
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fun blur() {
|
|
276
|
+
activeEditText?.clearFocus()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fun hasActiveComposition(): Boolean {
|
|
280
|
+
val editable = activeEditText?.text ?: return false
|
|
281
|
+
return BaseInputConnection.getComposingSpanStart(editable) >= 0
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fun currentScalarSelection(): Pair<Int, Int>? {
|
|
285
|
+
val selection = activeDocSelection() ?: return null
|
|
286
|
+
if (currentEditorId == 0L) return null
|
|
287
|
+
return editorDocToScalar(currentEditorId.toULong(), selection.first.toUInt()).toInt() to
|
|
288
|
+
editorDocToScalar(currentEditorId.toULong(), selection.second.toUInt()).toInt()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fun activeDocPositionForUtf16Offset(offset: Int): Int? {
|
|
292
|
+
val leaf = activeEditText as? TextLeaf ?: return null
|
|
293
|
+
return leaf.docPositionForOffset(offset)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fun setAutoCapitalize(value: String?) {
|
|
297
|
+
configuredAutoCapitalize = value
|
|
298
|
+
val flag = when (value) {
|
|
299
|
+
"none" -> 0
|
|
300
|
+
"words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
|
301
|
+
"characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
302
|
+
else -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
303
|
+
}
|
|
304
|
+
textLeafViews.forEach { leaf ->
|
|
305
|
+
leaf.inputType = (leaf.inputType and (
|
|
306
|
+
InputType.TYPE_TEXT_FLAG_CAP_WORDS or
|
|
307
|
+
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
|
|
308
|
+
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
309
|
+
).inv()) or flag
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fun setAutoCorrect(enabled: Boolean?) {
|
|
314
|
+
configuredAutoCorrect = enabled
|
|
315
|
+
textLeafViews.forEach { leaf ->
|
|
316
|
+
leaf.inputType = if (enabled == true) {
|
|
317
|
+
(leaf.inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS.inv()) or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
|
|
318
|
+
} else {
|
|
319
|
+
(leaf.inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT.inv()) or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fun setKeyboardType(value: String?) {
|
|
325
|
+
configuredKeyboardType = value
|
|
326
|
+
val inputClass = when (value) {
|
|
327
|
+
"numeric", "number-pad", "decimal-pad" -> InputType.TYPE_CLASS_NUMBER
|
|
328
|
+
"phone-pad" -> InputType.TYPE_CLASS_PHONE
|
|
329
|
+
else -> InputType.TYPE_CLASS_TEXT
|
|
330
|
+
}
|
|
331
|
+
textLeafViews.forEach { it.inputType = (it.inputType and InputType.TYPE_MASK_CLASS.inv()) or inputClass }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private fun activeDocSelection(): Pair<Int, Int>? {
|
|
335
|
+
val leaf = activeEditText as? TextLeaf ?: return null
|
|
336
|
+
val start = leaf.selectionStart.coerceAtLeast(0)
|
|
337
|
+
val end = leaf.selectionEnd.coerceAtLeast(start)
|
|
338
|
+
val anchor = leaf.docPositionForOffset(start) ?: return null
|
|
339
|
+
val head = leaf.docPositionForOffset(end) ?: return null
|
|
340
|
+
return anchor to head
|
|
341
|
+
}
|
|
342
|
+
|
|
181
343
|
init {
|
|
182
344
|
isFillViewport = false
|
|
183
345
|
clipToPadding = false
|
|
@@ -193,12 +355,6 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
193
355
|
override fun onTouchEvent(event: MotionEvent): Boolean =
|
|
194
356
|
isScrollingEnabled && super.onTouchEvent(event)
|
|
195
357
|
|
|
196
|
-
override fun dispatchDraw(canvas: Canvas) {
|
|
197
|
-
super.dispatchDraw(canvas)
|
|
198
|
-
if (!isEditable || !isInputFocused || selectedNodePos != null) return
|
|
199
|
-
selectedCaretRect()?.let { canvas.drawRect(it, caretPaint) }
|
|
200
|
-
}
|
|
201
|
-
|
|
202
358
|
fun bind(editorId: Long) {
|
|
203
359
|
currentEditorId = editorId
|
|
204
360
|
if (editorId == 0L) {
|
|
@@ -227,10 +383,26 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
227
383
|
val bottomInset = ((theme?.contentInsets?.bottom ?: 0f) * density).toInt()
|
|
228
384
|
val leftInset = ((theme?.contentInsets?.left ?: 0f) * density).toInt()
|
|
229
385
|
setPadding(leftInset, topInset, rightInset, bottomInset)
|
|
386
|
+
currentRoot?.let { root ->
|
|
387
|
+
val focusedSelection = activeDocSelection()
|
|
388
|
+
// Retire the old input connection before replacing themed leaves. Keeping
|
|
389
|
+
// a detached EditText as the IME target can make Android replay its text
|
|
390
|
+
// into the newly focused canonical leaf after a uiMode change.
|
|
391
|
+
if (focusedSelection != null) activeEditText?.clearFocus()
|
|
392
|
+
isApplyingRemoteUpdate = true
|
|
393
|
+
rebuildAll(root)
|
|
394
|
+
refreshTableCellRegistry()
|
|
395
|
+
isApplyingRemoteUpdate = false
|
|
396
|
+
focusedSelection?.let { applyNativeSelection(it.first, it.second, true) }
|
|
397
|
+
}
|
|
230
398
|
}
|
|
231
399
|
|
|
232
400
|
fun applyUpdateJSON(updateJSON: String, editorId: Long) {
|
|
401
|
+
val shouldPreserveTextFocus = textLeafViews.any { it.hasFocus() }
|
|
233
402
|
val update = try { JSONObject(updateJSON) } catch (_: Throwable) { return }
|
|
403
|
+
val textSelection = update.optJSONObject("selection")?.takeIf {
|
|
404
|
+
it.optString("type") == "text"
|
|
405
|
+
}?.let { it.optInt("anchor") to it.optInt("head") }
|
|
234
406
|
update.optJSONObject("selection")?.let { selection ->
|
|
235
407
|
selectedNodePos = if (selection.optString("type") == "node") {
|
|
236
408
|
selection.optInt("pos")
|
|
@@ -243,7 +415,11 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
243
415
|
) selection.optInt("anchor") else null
|
|
244
416
|
refreshImageSelection()
|
|
245
417
|
}
|
|
246
|
-
val root = update.optJSONObject("renderTree")?.optJSONObject("root")
|
|
418
|
+
val root = update.optJSONObject("renderTree")?.optJSONObject("root")
|
|
419
|
+
if (root == null) {
|
|
420
|
+
textSelection?.let { applyNativeSelection(it.first, it.second, shouldPreserveTextFocus) }
|
|
421
|
+
return
|
|
422
|
+
}
|
|
247
423
|
currentEditorId = editorId
|
|
248
424
|
isApplyingRemoteUpdate = true
|
|
249
425
|
val operations = update.optJSONObject("renderPatch")?.optJSONArray("operations")
|
|
@@ -256,6 +432,7 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
256
432
|
refreshImageSelection()
|
|
257
433
|
currentRoot = root
|
|
258
434
|
isApplyingRemoteUpdate = false
|
|
435
|
+
textSelection?.let { applyNativeSelection(it.first, it.second, shouldPreserveTextFocus) }
|
|
259
436
|
geometryRevision += 1
|
|
260
437
|
requestLayout()
|
|
261
438
|
}
|
|
@@ -345,6 +522,10 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
345
522
|
rebuildAll(root)
|
|
346
523
|
return
|
|
347
524
|
}
|
|
525
|
+
if (oldView is TextLeaf) {
|
|
526
|
+
configureTextLeaf(oldView, node)
|
|
527
|
+
return
|
|
528
|
+
}
|
|
348
529
|
val parent = oldView.parent as? ViewGroup ?: run {
|
|
349
530
|
rebuildAll(root)
|
|
350
531
|
return
|
|
@@ -429,6 +610,11 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
429
610
|
internal val topLevelViewCountForTesting: Int
|
|
430
611
|
get() = stack.childCount
|
|
431
612
|
|
|
613
|
+
internal fun textColorForTesting(value: String): Int? =
|
|
614
|
+
textLeafViews.firstOrNull { it.text?.toString() == value }?.currentTextColor
|
|
615
|
+
|
|
616
|
+
internal fun tableFillColorsForTesting(): List<Int> = tableCells.map { it.baseFillColor }
|
|
617
|
+
|
|
432
618
|
private fun viewForNode(node: JSONObject, depth: Int, path: List<Int>): View {
|
|
433
619
|
val rendered = when (node.optString("kind")) {
|
|
434
620
|
"list" -> listView(node, depth, path)
|
|
@@ -573,7 +759,11 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
573
759
|
val tableId = nextTableId++
|
|
574
760
|
val table = LinearLayout(context).apply {
|
|
575
761
|
orientation = LinearLayout.VERTICAL
|
|
576
|
-
background = borderDrawable(
|
|
762
|
+
background = borderDrawable(
|
|
763
|
+
Color.TRANSPARENT,
|
|
764
|
+
currentTheme?.table?.borderColor ?: baseTextColor,
|
|
765
|
+
1f
|
|
766
|
+
)
|
|
577
767
|
}
|
|
578
768
|
val rows = node.optJSONArray("children")
|
|
579
769
|
for (index in 0 until (rows?.length() ?: 0)) {
|
|
@@ -599,7 +789,15 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
599
789
|
|
|
600
790
|
private fun cellView(node: JSONObject, tableId: Int, rowIndex: Int, columnIndex: Int, path: List<Int>): View {
|
|
601
791
|
val density = resources.displayMetrics.density
|
|
602
|
-
val fillColor = if (node.optString("nodeType") == "tableHeader")
|
|
792
|
+
val fillColor = if (node.optString("nodeType") == "tableHeader") {
|
|
793
|
+
currentTheme?.table?.headerBackgroundColor
|
|
794
|
+
?: currentTheme?.codeBlock?.backgroundColor
|
|
795
|
+
?: Color.TRANSPARENT
|
|
796
|
+
} else {
|
|
797
|
+
currentTheme?.table?.cellBackgroundColor
|
|
798
|
+
?: currentTheme?.backgroundColor
|
|
799
|
+
?: Color.TRANSPARENT
|
|
800
|
+
}
|
|
603
801
|
val cell = CellView(context).apply {
|
|
604
802
|
docPos = node.optInt("docPos", 0)
|
|
605
803
|
this.tableId = tableId
|
|
@@ -610,7 +808,7 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
610
808
|
setPadding((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
|
|
611
809
|
background = borderDrawable(
|
|
612
810
|
fillColor,
|
|
613
|
-
|
|
811
|
+
currentTheme?.table?.borderColor ?: baseTextColor,
|
|
614
812
|
0.5f
|
|
615
813
|
)
|
|
616
814
|
isClickable = true
|
|
@@ -628,17 +826,29 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
628
826
|
}
|
|
629
827
|
|
|
630
828
|
private fun textBlockView(node: JSONObject, depth: Int): View {
|
|
829
|
+
return TextLeaf(context).also { configureTextLeaf(it, node) }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private fun configureTextLeaf(leaf: TextLeaf, node: JSONObject) {
|
|
631
833
|
val rendered = attributedInlineContent(node)
|
|
632
|
-
val
|
|
834
|
+
val hadFocus = leaf.hasFocus()
|
|
835
|
+
val previousSelectionStart = leaf.selectionStart.coerceAtLeast(0)
|
|
836
|
+
val previousSelectionEnd = leaf.selectionEnd.coerceAtLeast(previousSelectionStart)
|
|
837
|
+
leaf.apply leaf@ {
|
|
633
838
|
text = rendered.first
|
|
634
839
|
positionSegments = if (rendered.second.isEmpty()) {
|
|
635
|
-
listOf(PositionSegment(0, 0, "", node.optInt("docPos", 0)
|
|
840
|
+
listOf(PositionSegment(0, 0, "", node.optInt("docPos", 0) + if (node.optInt("docSize", 0) >= 2) 1 else 0, 0, false))
|
|
636
841
|
} else {
|
|
637
842
|
rendered.second
|
|
638
843
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
844
|
+
val style = currentTheme?.effectiveTextStyle(node.optString("nodeType"))
|
|
845
|
+
setTextColor(style?.color ?: baseTextColor)
|
|
846
|
+
setTextSize(
|
|
847
|
+
TypedValue.COMPLEX_UNIT_PX,
|
|
848
|
+
style?.fontSize?.let(::spToPx) ?: textSizeFor(node)
|
|
849
|
+
)
|
|
850
|
+
typeface = style?.let { Typeface.create(it.fontFamily, it.typefaceStyle()) }
|
|
851
|
+
?: typefaceFor(node)
|
|
642
852
|
minLines = 1
|
|
643
853
|
background = if (node.optString("nodeType") == "codeBlock" || node.optString("nodeType") == "code_block") {
|
|
644
854
|
roundedDrawable(
|
|
@@ -657,26 +867,59 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
657
867
|
} else {
|
|
658
868
|
setPadding(0, 0, 0, 0)
|
|
659
869
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
true
|
|
870
|
+
isEnabled = this@NativeBlockEditorSurface.isEditable
|
|
871
|
+
isFocusable = this@NativeBlockEditorSurface.isEditable
|
|
872
|
+
isFocusableInTouchMode = this@NativeBlockEditorSurface.isEditable
|
|
873
|
+
setTextIsSelectable(this@NativeBlockEditorSurface.isEditable)
|
|
874
|
+
isSingleLine = false
|
|
875
|
+
editorSurface = this@NativeBlockEditorSurface
|
|
876
|
+
updateLeafHint(this)
|
|
877
|
+
onFocusChangeListener = OnFocusChangeListener { _, focused ->
|
|
878
|
+
if (focused) onFocusChange?.invoke(true)
|
|
879
|
+
else if (!this@NativeBlockEditorSurface.isEditorFocused) onFocusChange?.invoke(false)
|
|
671
880
|
}
|
|
881
|
+
applyConfiguredInputTraits(this)
|
|
882
|
+
if (hadFocus) {
|
|
883
|
+
setSelection(
|
|
884
|
+
previousSelectionStart.coerceAtMost(length()),
|
|
885
|
+
previousSelectionEnd.coerceAtMost(length())
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private fun updateLeafHint(leaf: TextLeaf) {
|
|
892
|
+
leaf.hint = if ((textLeafViews.firstOrNull() == null || textLeafViews.firstOrNull() === leaf) && leaf.text.isNullOrEmpty()) {
|
|
893
|
+
placeholder
|
|
894
|
+
} else {
|
|
895
|
+
null
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private fun applyConfiguredInputTraits(leaf: TextLeaf) {
|
|
900
|
+
val capitalFlag = when (configuredAutoCapitalize) {
|
|
901
|
+
"none" -> 0
|
|
902
|
+
"words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
|
903
|
+
"characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
904
|
+
else -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
905
|
+
}
|
|
906
|
+
val inputClass = when (configuredKeyboardType) {
|
|
907
|
+
"numeric", "number-pad", "decimal-pad" -> InputType.TYPE_CLASS_NUMBER
|
|
908
|
+
"phone-pad" -> InputType.TYPE_CLASS_PHONE
|
|
909
|
+
else -> InputType.TYPE_CLASS_TEXT
|
|
910
|
+
}
|
|
911
|
+
val suggestionFlag = if (configuredAutoCorrect == true) {
|
|
912
|
+
InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
|
|
913
|
+
} else {
|
|
914
|
+
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
|
672
915
|
}
|
|
673
|
-
|
|
916
|
+
leaf.inputType = inputClass or capitalFlag or suggestionFlag or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
674
917
|
}
|
|
675
918
|
|
|
676
919
|
private fun positionSegmentsFor(node: JSONObject): List<PositionSegment> {
|
|
677
920
|
val segments = attributedInlineContent(node).second
|
|
678
921
|
return if (segments.isEmpty()) {
|
|
679
|
-
listOf(PositionSegment(0, 0, "", node.optInt("docPos", 0)
|
|
922
|
+
listOf(PositionSegment(0, 0, "", node.optInt("docPos", 0) + if (node.optInt("docSize", 0) >= 2) 1 else 0, 0, false))
|
|
680
923
|
} else {
|
|
681
924
|
segments
|
|
682
925
|
}
|
|
@@ -779,10 +1022,14 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
779
1022
|
}
|
|
780
1023
|
return TextView(context).apply {
|
|
781
1024
|
text = node.optString("label", node.optString("nodeType", "Block"))
|
|
782
|
-
setTextColor(
|
|
1025
|
+
setTextColor(currentTheme?.text?.color ?: baseTextColor)
|
|
783
1026
|
textSize = 14f
|
|
784
1027
|
setPadding(12, 8, 12, 8)
|
|
785
|
-
background = borderDrawable(
|
|
1028
|
+
background = borderDrawable(
|
|
1029
|
+
currentTheme?.codeBlock?.backgroundColor ?: Color.TRANSPARENT,
|
|
1030
|
+
Color.TRANSPARENT,
|
|
1031
|
+
0f
|
|
1032
|
+
)
|
|
786
1033
|
}
|
|
787
1034
|
}
|
|
788
1035
|
|
|
@@ -800,7 +1047,12 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
800
1047
|
?.let { (it * density).toInt() }
|
|
801
1048
|
scaleType = ImageView.ScaleType.FIT_CENTER
|
|
802
1049
|
adjustViewBounds = true
|
|
803
|
-
background = roundedDrawable(
|
|
1050
|
+
background = roundedDrawable(
|
|
1051
|
+
currentTheme?.codeBlock?.backgroundColor ?: Color.TRANSPARENT,
|
|
1052
|
+
Color.TRANSPARENT,
|
|
1053
|
+
0f,
|
|
1054
|
+
8f
|
|
1055
|
+
)
|
|
804
1056
|
isClickable = true
|
|
805
1057
|
setOnClickListener { selectImage(this) }
|
|
806
1058
|
}
|
|
@@ -834,13 +1086,13 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
834
1086
|
}
|
|
835
1087
|
}
|
|
836
1088
|
|
|
837
|
-
internal fun selectedImageGeometry():
|
|
1089
|
+
internal fun selectedImageGeometry(): SelectedImageGeometry? {
|
|
838
1090
|
val docPos = selectedNodePos ?: return null
|
|
839
1091
|
val imageView = imageViews.firstOrNull { it.docPos == docPos } ?: return null
|
|
840
1092
|
if (!isEditable || imageView.width <= 0 || imageView.height <= 0) return null
|
|
841
1093
|
val rect = Rect(0, 0, imageView.width, imageView.height)
|
|
842
1094
|
offsetDescendantRectToMyCoords(imageView, rect)
|
|
843
|
-
return
|
|
1095
|
+
return SelectedImageGeometry(docPos, RectF(rect))
|
|
844
1096
|
}
|
|
845
1097
|
|
|
846
1098
|
internal fun selectedCaretRect(): RectF? =
|
|
@@ -874,6 +1126,80 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
874
1126
|
}
|
|
875
1127
|
}
|
|
876
1128
|
|
|
1129
|
+
private fun applyNativeSelection(anchor: Int, head: Int, preservingFocus: Boolean) {
|
|
1130
|
+
val candidates = textLeafViews.filter {
|
|
1131
|
+
it.nativeOffsetForDocPosition(anchor) != null &&
|
|
1132
|
+
it.nativeOffsetForDocPosition(head) != null
|
|
1133
|
+
}
|
|
1134
|
+
val leaf = candidates.firstOrNull { it.hasFocus() }
|
|
1135
|
+
?: (if (anchor == head) candidates.lastOrNull() else candidates.firstOrNull())
|
|
1136
|
+
?: return
|
|
1137
|
+
val start = leaf.nativeOffsetForDocPosition(minOf(anchor, head)) ?: return
|
|
1138
|
+
val end = leaf.nativeOffsetForDocPosition(maxOf(anchor, head)) ?: return
|
|
1139
|
+
isApplyingRemoteUpdate = true
|
|
1140
|
+
try {
|
|
1141
|
+
leaf.setSelection(start.coerceIn(0, leaf.length()), end.coerceIn(0, leaf.length()))
|
|
1142
|
+
if (preservingFocus && !leaf.hasFocus()) leaf.requestFocus()
|
|
1143
|
+
} finally {
|
|
1144
|
+
isApplyingRemoteUpdate = false
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
private fun selectionChanged(leaf: TextLeaf, start: Int, end: Int) {
|
|
1149
|
+
if (isApplyingRemoteUpdate || !isEditable || currentEditorId == 0L) return
|
|
1150
|
+
val anchor = leaf.docPositionForOffset(start.coerceAtLeast(0)) ?: return
|
|
1151
|
+
val head = leaf.docPositionForOffset(end.coerceAtLeast(start)) ?: return
|
|
1152
|
+
editorSetSelection(currentEditorId.toULong(), anchor.toUInt(), head.toUInt())
|
|
1153
|
+
onSelectionChange?.invoke(anchor, head)
|
|
1154
|
+
onAppliedUpdate?.invoke(editorGetSelectionState(currentEditorId.toULong()))
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
private fun commitLeafTextMutation(leaf: TextLeaf, before: String, after: String) {
|
|
1158
|
+
if (isApplyingRemoteUpdate || !isEditable || currentEditorId == 0L || before == after) return
|
|
1159
|
+
var prefix = 0
|
|
1160
|
+
val prefixLimit = minOf(before.length, after.length)
|
|
1161
|
+
while (prefix < prefixLimit && before[prefix] == after[prefix]) prefix += 1
|
|
1162
|
+
var suffix = 0
|
|
1163
|
+
while (
|
|
1164
|
+
suffix < before.length - prefix &&
|
|
1165
|
+
suffix < after.length - prefix &&
|
|
1166
|
+
before[before.length - 1 - suffix] == after[after.length - 1 - suffix]
|
|
1167
|
+
) suffix += 1
|
|
1168
|
+
|
|
1169
|
+
val oldEnd = before.length - suffix
|
|
1170
|
+
val newEnd = after.length - suffix
|
|
1171
|
+
val anchor = leaf.docPositionForOffset(prefix) ?: return
|
|
1172
|
+
val head = leaf.docPositionForOffset(oldEnd) ?: return
|
|
1173
|
+
val replacement = after.substring(prefix, newEnd)
|
|
1174
|
+
editorSetSelection(currentEditorId.toULong(), anchor.toUInt(), head.toUInt())
|
|
1175
|
+
val update = if (replacement == "\n") {
|
|
1176
|
+
if (anchor != head) {
|
|
1177
|
+
editorReplaceSelectionText(currentEditorId.toULong(), "")
|
|
1178
|
+
}
|
|
1179
|
+
editorSplitBlock(currentEditorId.toULong(), anchor.toUInt())
|
|
1180
|
+
} else {
|
|
1181
|
+
editorReplaceSelectionText(currentEditorId.toULong(), replacement)
|
|
1182
|
+
}
|
|
1183
|
+
applyUpdateJSON(update, currentEditorId)
|
|
1184
|
+
onEditorUpdate?.invoke(update)
|
|
1185
|
+
onAppliedUpdate?.invoke(update)
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
private fun deleteBackwardAtLeafBoundary(leaf: TextLeaf): Boolean {
|
|
1189
|
+
if (isApplyingRemoteUpdate || !isEditable || currentEditorId == 0L) return false
|
|
1190
|
+
val docPosition = leaf.docPositionForOffset(0) ?: return false
|
|
1191
|
+
val scalar = editorDocToScalar(currentEditorId.toULong(), docPosition.toUInt())
|
|
1192
|
+
val update = editorDeleteBackwardAtSelectionScalar(
|
|
1193
|
+
currentEditorId.toULong(),
|
|
1194
|
+
scalar,
|
|
1195
|
+
scalar
|
|
1196
|
+
)
|
|
1197
|
+
applyUpdateJSON(update, currentEditorId)
|
|
1198
|
+
onEditorUpdate?.invoke(update)
|
|
1199
|
+
onAppliedUpdate?.invoke(update)
|
|
1200
|
+
return true
|
|
1201
|
+
}
|
|
1202
|
+
|
|
877
1203
|
private fun descendantRectInSurface(descendant: View, local: RectF): RectF {
|
|
878
1204
|
val origin = Rect(0, 0, 0, 0)
|
|
879
1205
|
offsetDescendantRectToMyCoords(descendant, origin)
|
|
@@ -993,8 +1319,14 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
993
1319
|
cell == anchor || cell == head
|
|
994
1320
|
}
|
|
995
1321
|
cell.background = borderDrawable(
|
|
996
|
-
if (selected)
|
|
997
|
-
|
|
1322
|
+
if (selected) {
|
|
1323
|
+
currentTheme?.table?.selectionBackgroundColor ?: cell.baseFillColor
|
|
1324
|
+
} else cell.baseFillColor,
|
|
1325
|
+
if (selected) {
|
|
1326
|
+
currentTheme?.table?.selectionBorderColor ?: baseTextColor
|
|
1327
|
+
} else {
|
|
1328
|
+
currentTheme?.table?.borderColor ?: baseTextColor
|
|
1329
|
+
},
|
|
998
1330
|
if (selected) 1.5f else 0.5f
|
|
999
1331
|
)
|
|
1000
1332
|
}
|