@apollohg/react-native-prose-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +160 -0
- package/README.md +143 -0
- package/android/build.gradle +39 -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/EditorAddons.kt +131 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +228 -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 +254 -0
- package/ios/EditorTheme.swift +372 -0
- package/ios/Generated_editor_core.swift +1143 -0
- package/ios/NativeEditorExpoView.swift +1417 -0
- package/ios/NativeEditorModule.swift +263 -0
- package/ios/PositionBridge.swift +278 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +825 -0
- package/ios/RichTextEditorView.swift +1559 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1014 -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 +66 -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 +2014 -0
- package/src/EditorTheme.ts +130 -0
- package/src/EditorToolbar.tsx +620 -0
- package/src/NativeEditorBridge.ts +607 -0
- package/src/NativeRichTextEditor.tsx +951 -0
- package/src/addons.ts +158 -0
- package/src/index.ts +63 -0
- package/src/schemas.ts +153 -0
- package/src/useNativeEditor.ts +173 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
package com.apollohg.editor
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.ContextWrapper
|
|
6
|
+
import android.graphics.Rect
|
|
7
|
+
import android.graphics.RectF
|
|
8
|
+
import android.view.Gravity
|
|
9
|
+
import android.view.MotionEvent
|
|
10
|
+
import android.view.View
|
|
11
|
+
import android.view.ViewGroup
|
|
12
|
+
import android.view.Window
|
|
13
|
+
import android.view.inputmethod.InputMethodManager
|
|
14
|
+
import android.widget.FrameLayout
|
|
15
|
+
import androidx.core.view.ViewCompat
|
|
16
|
+
import androidx.core.view.WindowInsetsCompat
|
|
17
|
+
import expo.modules.kotlin.AppContext
|
|
18
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
19
|
+
import expo.modules.kotlin.views.ExpoView
|
|
20
|
+
import org.json.JSONArray
|
|
21
|
+
import org.json.JSONObject
|
|
22
|
+
import uniffi.editor_core.*
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Expo Modules wrapper view that hosts a [RichTextEditorView] and bridges
|
|
26
|
+
* editor events to React Native via [EventDispatcher].
|
|
27
|
+
*
|
|
28
|
+
* Registered as the native view component in [NativeEditorModule].
|
|
29
|
+
*/
|
|
30
|
+
class NativeEditorExpoView(
|
|
31
|
+
context: Context,
|
|
32
|
+
appContext: AppContext
|
|
33
|
+
) : ExpoView(context, appContext), EditorEditText.EditorListener {
|
|
34
|
+
|
|
35
|
+
private enum class ToolbarPlacement {
|
|
36
|
+
KEYBOARD,
|
|
37
|
+
INLINE;
|
|
38
|
+
|
|
39
|
+
companion object {
|
|
40
|
+
fun fromRaw(raw: String?): ToolbarPlacement =
|
|
41
|
+
if (raw == "inline") INLINE else KEYBOARD
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
val richTextView: RichTextEditorView = RichTextEditorView(context)
|
|
46
|
+
private val keyboardToolbarView = EditorKeyboardToolbarView(context)
|
|
47
|
+
|
|
48
|
+
private val onEditorUpdate by EventDispatcher<Map<String, Any>>()
|
|
49
|
+
private val onSelectionChange by EventDispatcher<Map<String, Any>>()
|
|
50
|
+
private val onFocusChange by EventDispatcher<Map<String, Any>>()
|
|
51
|
+
private val onContentHeightChange by EventDispatcher<Map<String, Any>>()
|
|
52
|
+
@Suppress("unused")
|
|
53
|
+
private val onToolbarAction by EventDispatcher<Map<String, Any>>()
|
|
54
|
+
@Suppress("unused")
|
|
55
|
+
private val onAddonEvent by EventDispatcher<Map<String, Any>>()
|
|
56
|
+
|
|
57
|
+
/** Guard flag: when true, editor updates originated from JS and should not echo back. */
|
|
58
|
+
var isApplyingJSUpdate = false
|
|
59
|
+
private var didApplyAutoFocus = false
|
|
60
|
+
private var heightBehavior = EditorHeightBehavior.FIXED
|
|
61
|
+
private var lastEmittedContentHeight = 0
|
|
62
|
+
private var outsideTapWindowCallback: Window.Callback? = null
|
|
63
|
+
private var previousWindowCallback: Window.Callback? = null
|
|
64
|
+
private var toolbarFrameInWindow: RectF? = null
|
|
65
|
+
private var addons = NativeEditorAddons(null)
|
|
66
|
+
private var mentionQueryState: MentionQueryState? = null
|
|
67
|
+
private var lastMentionEventJson: String? = null
|
|
68
|
+
private var toolbarState = NativeToolbarState.empty
|
|
69
|
+
private var showsToolbar = true
|
|
70
|
+
private var toolbarPlacement = ToolbarPlacement.KEYBOARD
|
|
71
|
+
private var currentImeBottom = 0
|
|
72
|
+
private var pendingEditorUpdateJson: String? = null
|
|
73
|
+
private var pendingEditorUpdateRevision = 0
|
|
74
|
+
private var appliedEditorUpdateRevision = 0
|
|
75
|
+
|
|
76
|
+
init {
|
|
77
|
+
addView(richTextView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
78
|
+
richTextView.editorEditText.editorListener = this
|
|
79
|
+
keyboardToolbarView.onPressItem = { item ->
|
|
80
|
+
handleToolbarItemPress(item)
|
|
81
|
+
}
|
|
82
|
+
keyboardToolbarView.onSelectMentionSuggestion = { suggestion ->
|
|
83
|
+
insertMentionSuggestion(suggestion)
|
|
84
|
+
}
|
|
85
|
+
keyboardToolbarView.applyState(toolbarState)
|
|
86
|
+
ViewCompat.setOnApplyWindowInsetsListener(keyboardToolbarView) { _, insets ->
|
|
87
|
+
currentImeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
88
|
+
updateKeyboardToolbarLayout()
|
|
89
|
+
updateKeyboardToolbarVisibility()
|
|
90
|
+
insets
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Observe EditText focus changes.
|
|
94
|
+
richTextView.editorEditText.setOnFocusChangeListener { _, hasFocus ->
|
|
95
|
+
if (hasFocus) {
|
|
96
|
+
installOutsideTapBlurHandlerIfNeeded()
|
|
97
|
+
refreshMentionQuery()
|
|
98
|
+
} else {
|
|
99
|
+
uninstallOutsideTapBlurHandler()
|
|
100
|
+
clearMentionQueryState()
|
|
101
|
+
}
|
|
102
|
+
updateKeyboardToolbarVisibility()
|
|
103
|
+
val event = mapOf<String, Any>("isFocused" to hasFocus)
|
|
104
|
+
onFocusChange(event)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fun setEditorId(id: Long) {
|
|
109
|
+
richTextView.editorId = id
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fun setThemeJson(themeJson: String?) {
|
|
113
|
+
val theme = EditorTheme.fromJson(themeJson)
|
|
114
|
+
richTextView.applyTheme(theme)
|
|
115
|
+
keyboardToolbarView.applyTheme(theme?.toolbar)
|
|
116
|
+
keyboardToolbarView.applyMentionTheme(theme?.mentions ?: addons.mentions?.theme)
|
|
117
|
+
updateKeyboardToolbarLayout()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fun setHeightBehavior(rawHeightBehavior: String) {
|
|
121
|
+
val nextBehavior = EditorHeightBehavior.fromRaw(rawHeightBehavior)
|
|
122
|
+
if (heightBehavior == nextBehavior) return
|
|
123
|
+
heightBehavior = nextBehavior
|
|
124
|
+
if (nextBehavior != EditorHeightBehavior.AUTO_GROW) {
|
|
125
|
+
lastEmittedContentHeight = 0
|
|
126
|
+
}
|
|
127
|
+
richTextView.setHeightBehavior(nextBehavior)
|
|
128
|
+
val params = richTextView.layoutParams as LayoutParams
|
|
129
|
+
params.width = LayoutParams.MATCH_PARENT
|
|
130
|
+
params.height = if (nextBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
131
|
+
LayoutParams.WRAP_CONTENT
|
|
132
|
+
} else {
|
|
133
|
+
LayoutParams.MATCH_PARENT
|
|
134
|
+
}
|
|
135
|
+
richTextView.layoutParams = params
|
|
136
|
+
requestLayout()
|
|
137
|
+
if (nextBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
138
|
+
post { emitContentHeightIfNeeded(force = true) }
|
|
139
|
+
}
|
|
140
|
+
updateEditorViewportInset()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fun setAddonsJson(addonsJson: String?) {
|
|
144
|
+
addons = NativeEditorAddons.fromJson(addonsJson)
|
|
145
|
+
keyboardToolbarView.applyMentionTheme(richTextView.editorEditText.theme?.mentions ?: addons.mentions?.theme)
|
|
146
|
+
refreshMentionQuery()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fun setAutoFocus(autoFocus: Boolean) {
|
|
150
|
+
if (!autoFocus || didApplyAutoFocus) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
didApplyAutoFocus = true
|
|
154
|
+
focus()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fun setShowToolbar(showToolbar: Boolean) {
|
|
158
|
+
showsToolbar = showToolbar
|
|
159
|
+
updateKeyboardToolbarVisibility()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fun setToolbarPlacement(rawToolbarPlacement: String?) {
|
|
163
|
+
toolbarPlacement = ToolbarPlacement.fromRaw(rawToolbarPlacement)
|
|
164
|
+
updateKeyboardToolbarVisibility()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fun setToolbarItemsJson(toolbarItemsJson: String?) {
|
|
168
|
+
keyboardToolbarView.setItems(NativeToolbarItem.fromJson(toolbarItemsJson))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fun setToolbarFrameJson(toolbarFrameJson: String?) {
|
|
172
|
+
if (toolbarFrameJson.isNullOrBlank()) {
|
|
173
|
+
toolbarFrameInWindow = null
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
toolbarFrameInWindow = try {
|
|
178
|
+
val json = JSONObject(toolbarFrameJson)
|
|
179
|
+
RectF(
|
|
180
|
+
json.optDouble("x").toFloat(),
|
|
181
|
+
json.optDouble("y").toFloat(),
|
|
182
|
+
(json.optDouble("x") + json.optDouble("width")).toFloat(),
|
|
183
|
+
(json.optDouble("y") + json.optDouble("height")).toFloat()
|
|
184
|
+
)
|
|
185
|
+
} catch (_: Throwable) {
|
|
186
|
+
null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fun setPendingEditorUpdateJson(editorUpdateJson: String?) {
|
|
191
|
+
pendingEditorUpdateJson = editorUpdateJson
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fun setPendingEditorUpdateRevision(editorUpdateRevision: Int) {
|
|
195
|
+
pendingEditorUpdateRevision = editorUpdateRevision
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fun applyPendingEditorUpdateIfNeeded() {
|
|
199
|
+
val updateJson = pendingEditorUpdateJson ?: return
|
|
200
|
+
if (pendingEditorUpdateRevision == 0) return
|
|
201
|
+
if (pendingEditorUpdateRevision == appliedEditorUpdateRevision) return
|
|
202
|
+
appliedEditorUpdateRevision = pendingEditorUpdateRevision
|
|
203
|
+
applyEditorUpdate(updateJson)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fun focus() {
|
|
207
|
+
richTextView.editorEditText.requestFocus()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fun blur() {
|
|
211
|
+
richTextView.editorEditText.clearFocus()
|
|
212
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
213
|
+
imm?.hideSoftInputFromWindow(richTextView.editorEditText.windowToken, 0)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
override fun onDetachedFromWindow() {
|
|
217
|
+
super.onDetachedFromWindow()
|
|
218
|
+
uninstallOutsideTapBlurHandler()
|
|
219
|
+
detachKeyboardToolbarIfNeeded()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
223
|
+
if (heightBehavior != EditorHeightBehavior.AUTO_GROW) {
|
|
224
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
val childWidthSpec = getChildMeasureSpec(
|
|
229
|
+
widthMeasureSpec,
|
|
230
|
+
paddingLeft + paddingRight,
|
|
231
|
+
richTextView.layoutParams.width
|
|
232
|
+
)
|
|
233
|
+
val childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
234
|
+
richTextView.measure(childWidthSpec, childHeightSpec)
|
|
235
|
+
|
|
236
|
+
val measuredWidth = resolveSize(
|
|
237
|
+
richTextView.measuredWidth + paddingLeft + paddingRight,
|
|
238
|
+
widthMeasureSpec
|
|
239
|
+
)
|
|
240
|
+
val desiredHeight = richTextView.measuredHeight + paddingTop + paddingBottom
|
|
241
|
+
val measuredHeight = when (MeasureSpec.getMode(heightMeasureSpec)) {
|
|
242
|
+
MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
|
|
243
|
+
else -> desiredHeight
|
|
244
|
+
}
|
|
245
|
+
setMeasuredDimension(measuredWidth, measuredHeight)
|
|
246
|
+
emitContentHeightIfNeeded(force = false)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private fun emitContentHeightIfNeeded(force: Boolean) {
|
|
250
|
+
if (heightBehavior != EditorHeightBehavior.AUTO_GROW) return
|
|
251
|
+
val editText = richTextView.editorEditText
|
|
252
|
+
val resolvedEditHeight = editText.resolveAutoGrowHeight()
|
|
253
|
+
val contentHeight = (
|
|
254
|
+
when {
|
|
255
|
+
editText.isLaidOut && (editText.layout?.height ?: 0) > 0 -> {
|
|
256
|
+
(editText.layout?.height ?: 0) +
|
|
257
|
+
editText.compoundPaddingTop +
|
|
258
|
+
editText.compoundPaddingBottom +
|
|
259
|
+
richTextView.paddingTop +
|
|
260
|
+
richTextView.paddingBottom
|
|
261
|
+
}
|
|
262
|
+
richTextView.measuredHeight > 0 -> {
|
|
263
|
+
richTextView.measuredHeight + paddingTop + paddingBottom
|
|
264
|
+
}
|
|
265
|
+
editText.measuredHeight > 0 -> {
|
|
266
|
+
editText.measuredHeight +
|
|
267
|
+
richTextView.paddingTop +
|
|
268
|
+
richTextView.paddingBottom +
|
|
269
|
+
paddingTop +
|
|
270
|
+
paddingBottom
|
|
271
|
+
}
|
|
272
|
+
else -> {
|
|
273
|
+
resolvedEditHeight +
|
|
274
|
+
richTextView.paddingTop +
|
|
275
|
+
richTextView.paddingBottom +
|
|
276
|
+
paddingTop +
|
|
277
|
+
paddingBottom
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
).coerceAtLeast(0)
|
|
281
|
+
if (contentHeight <= 0) return
|
|
282
|
+
if (!force && contentHeight == lastEmittedContentHeight) return
|
|
283
|
+
lastEmittedContentHeight = contentHeight
|
|
284
|
+
onContentHeightChange(mapOf("contentHeight" to contentHeight))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Applies an editor update from JS without echoing it back through events. */
|
|
288
|
+
fun applyEditorUpdate(updateJson: String) {
|
|
289
|
+
val apply = Runnable {
|
|
290
|
+
isApplyingJSUpdate = true
|
|
291
|
+
richTextView.editorEditText.applyUpdateJSON(updateJson)
|
|
292
|
+
isApplyingJSUpdate = false
|
|
293
|
+
}
|
|
294
|
+
if (android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) {
|
|
295
|
+
apply.run()
|
|
296
|
+
} else {
|
|
297
|
+
val latch = java.util.concurrent.CountDownLatch(1)
|
|
298
|
+
post {
|
|
299
|
+
apply.run()
|
|
300
|
+
latch.countDown()
|
|
301
|
+
}
|
|
302
|
+
latch.await()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
override fun onSelectionChanged(anchor: Int, head: Int) {
|
|
307
|
+
refreshToolbarStateFromEditorSelection()
|
|
308
|
+
refreshMentionQuery()
|
|
309
|
+
val event = mapOf<String, Any>("anchor" to anchor, "head" to head)
|
|
310
|
+
onSelectionChange(event)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
override fun onEditorUpdate(updateJSON: String) {
|
|
314
|
+
NativeToolbarState.fromUpdateJson(updateJSON)?.let { state ->
|
|
315
|
+
toolbarState = state
|
|
316
|
+
keyboardToolbarView.applyState(state)
|
|
317
|
+
}
|
|
318
|
+
refreshMentionQuery()
|
|
319
|
+
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
320
|
+
post {
|
|
321
|
+
requestLayout()
|
|
322
|
+
emitContentHeightIfNeeded(force = false)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (isApplyingJSUpdate) return
|
|
326
|
+
val event = mapOf<String, Any>("updateJson" to updateJSON)
|
|
327
|
+
onEditorUpdate(event)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private fun installOutsideTapBlurHandlerIfNeeded() {
|
|
331
|
+
val window = resolveActivity(context)?.window ?: return
|
|
332
|
+
val currentCallback = window.callback ?: return
|
|
333
|
+
if (currentCallback === outsideTapWindowCallback) return
|
|
334
|
+
|
|
335
|
+
val wrappedCallback = object : Window.Callback by currentCallback {
|
|
336
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
337
|
+
if (
|
|
338
|
+
event.action == MotionEvent.ACTION_DOWN &&
|
|
339
|
+
richTextView.editorEditText.hasFocus() &&
|
|
340
|
+
isTouchOutsideEditor(event)
|
|
341
|
+
) {
|
|
342
|
+
blur()
|
|
343
|
+
}
|
|
344
|
+
return currentCallback.dispatchTouchEvent(event)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
previousWindowCallback = currentCallback
|
|
349
|
+
outsideTapWindowCallback = wrappedCallback
|
|
350
|
+
window.callback = wrappedCallback
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private fun uninstallOutsideTapBlurHandler() {
|
|
354
|
+
val window = resolveActivity(context)?.window ?: return
|
|
355
|
+
val callback = outsideTapWindowCallback ?: return
|
|
356
|
+
if (window.callback === callback) {
|
|
357
|
+
window.callback = previousWindowCallback ?: callback
|
|
358
|
+
}
|
|
359
|
+
outsideTapWindowCallback = null
|
|
360
|
+
previousWindowCallback = null
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private fun isTouchOutsideEditor(event: MotionEvent): Boolean {
|
|
364
|
+
if (isTouchInsideKeyboardToolbar(event)) {
|
|
365
|
+
return false
|
|
366
|
+
}
|
|
367
|
+
val toolbarFrame = toolbarFrameInWindow
|
|
368
|
+
if (toolbarFrame != null && toolbarFrame.contains(event.rawX, event.rawY)) {
|
|
369
|
+
return false
|
|
370
|
+
}
|
|
371
|
+
val rect = Rect()
|
|
372
|
+
richTextView.editorEditText.getGlobalVisibleRect(rect)
|
|
373
|
+
return !rect.contains(event.rawX.toInt(), event.rawY.toInt())
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private fun isTouchInsideKeyboardToolbar(event: MotionEvent): Boolean {
|
|
377
|
+
if (keyboardToolbarView.parent == null || keyboardToolbarView.visibility != View.VISIBLE) {
|
|
378
|
+
return false
|
|
379
|
+
}
|
|
380
|
+
val rect = Rect()
|
|
381
|
+
keyboardToolbarView.getGlobalVisibleRect(rect)
|
|
382
|
+
return rect.contains(event.rawX.toInt(), event.rawY.toInt())
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private fun resolveActivity(context: Context): Activity? {
|
|
386
|
+
var current: Context? = context
|
|
387
|
+
while (current is ContextWrapper) {
|
|
388
|
+
if (current is Activity) return current
|
|
389
|
+
current = current.baseContext
|
|
390
|
+
}
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private fun refreshMentionQuery() {
|
|
395
|
+
val mentions = addons.mentions
|
|
396
|
+
if (mentions == null || !richTextView.editorEditText.hasFocus()) {
|
|
397
|
+
clearMentionQueryState()
|
|
398
|
+
emitMentionQueryChange("", "@", 0, 0, false)
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
val queryState = currentMentionQueryState(mentions.trigger)
|
|
403
|
+
if (queryState == null) {
|
|
404
|
+
clearMentionQueryState()
|
|
405
|
+
emitMentionQueryChange("", mentions.trigger, 0, 0, false)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
mentionQueryState = queryState
|
|
410
|
+
val suggestions = filteredMentionSuggestions(queryState, mentions)
|
|
411
|
+
keyboardToolbarView.applyMentionTheme(richTextView.editorEditText.theme?.mentions ?: mentions.theme)
|
|
412
|
+
syncKeyboardToolbarMentionSuggestions(suggestions)
|
|
413
|
+
emitMentionQueryChange(
|
|
414
|
+
queryState.query,
|
|
415
|
+
queryState.trigger,
|
|
416
|
+
queryState.anchor,
|
|
417
|
+
queryState.head,
|
|
418
|
+
true
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private fun clearMentionQueryState() {
|
|
423
|
+
mentionQueryState = null
|
|
424
|
+
syncKeyboardToolbarMentionSuggestions(emptyList())
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private fun currentMentionQueryState(trigger: String): MentionQueryState? {
|
|
428
|
+
val editor = richTextView.editorEditText
|
|
429
|
+
if (editor.selectionStart != editor.selectionEnd) return null
|
|
430
|
+
val text = editor.text?.toString() ?: return null
|
|
431
|
+
val cursorUtf16 = editor.selectionStart
|
|
432
|
+
val cursorScalar = PositionBridge.utf16ToScalar(cursorUtf16, text)
|
|
433
|
+
return resolveMentionQueryState(
|
|
434
|
+
text = text,
|
|
435
|
+
cursorScalar = cursorScalar,
|
|
436
|
+
trigger = trigger,
|
|
437
|
+
isCaretInsideMention = isCaretInsideMention(cursorUtf16)
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private fun isCaretInsideMention(cursorUtf16: Int): Boolean {
|
|
442
|
+
val editable = richTextView.editorEditText.text ?: return false
|
|
443
|
+
val checkOffsets = listOf(cursorUtf16, (cursorUtf16 - 1).coerceAtLeast(0))
|
|
444
|
+
return checkOffsets.any { offset ->
|
|
445
|
+
editable.getSpans(offset, offset, android.text.Annotation::class.java).any { span ->
|
|
446
|
+
span.key == "nativeVoidNodeType" && span.value == "mention"
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private fun filteredMentionSuggestions(
|
|
452
|
+
queryState: MentionQueryState,
|
|
453
|
+
config: NativeMentionsAddonConfig
|
|
454
|
+
): List<NativeMentionSuggestion> {
|
|
455
|
+
val normalizedQuery = queryState.query.trim().lowercase()
|
|
456
|
+
if (normalizedQuery.isEmpty()) return config.suggestions
|
|
457
|
+
return config.suggestions.filter { suggestion ->
|
|
458
|
+
suggestion.title.lowercase().contains(normalizedQuery) ||
|
|
459
|
+
suggestion.label.lowercase().contains(normalizedQuery) ||
|
|
460
|
+
(suggestion.subtitle?.lowercase()?.contains(normalizedQuery) == true)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private fun syncKeyboardToolbarMentionSuggestions(suggestions: List<NativeMentionSuggestion>) {
|
|
465
|
+
keyboardToolbarView.setMentionSuggestions(suggestions)
|
|
466
|
+
keyboardToolbarView.requestLayout()
|
|
467
|
+
post {
|
|
468
|
+
updateKeyboardToolbarLayout()
|
|
469
|
+
updateEditorViewportInset()
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private fun emitMentionQueryChange(
|
|
474
|
+
query: String,
|
|
475
|
+
trigger: String,
|
|
476
|
+
anchor: Int,
|
|
477
|
+
head: Int,
|
|
478
|
+
isActive: Boolean
|
|
479
|
+
) {
|
|
480
|
+
val eventJson = JSONObject()
|
|
481
|
+
.put("type", "mentionsQueryChange")
|
|
482
|
+
.put("query", query)
|
|
483
|
+
.put("trigger", trigger)
|
|
484
|
+
.put("range", JSONObject().put("anchor", anchor).put("head", head))
|
|
485
|
+
.put("isActive", isActive)
|
|
486
|
+
.toString()
|
|
487
|
+
if (eventJson == lastMentionEventJson) return
|
|
488
|
+
lastMentionEventJson = eventJson
|
|
489
|
+
onAddonEvent(mapOf("eventJson" to eventJson))
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private fun emitMentionSelect(trigger: String, suggestion: NativeMentionSuggestion) {
|
|
493
|
+
val eventJson = JSONObject()
|
|
494
|
+
.put("type", "mentionsSelect")
|
|
495
|
+
.put("trigger", trigger)
|
|
496
|
+
.put("suggestionKey", suggestion.key)
|
|
497
|
+
.put("attrs", suggestion.attrs)
|
|
498
|
+
.toString()
|
|
499
|
+
onAddonEvent(mapOf("eventJson" to eventJson))
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private fun insertMentionSuggestion(suggestion: NativeMentionSuggestion) {
|
|
503
|
+
val mentions = addons.mentions ?: return
|
|
504
|
+
val queryState = mentionQueryState ?: return
|
|
505
|
+
val attrs = JSONObject(suggestion.attrs.toString())
|
|
506
|
+
if (!attrs.has("label")) {
|
|
507
|
+
attrs.put("label", suggestion.label)
|
|
508
|
+
}
|
|
509
|
+
val docJson = JSONObject()
|
|
510
|
+
.put("type", "doc")
|
|
511
|
+
.put(
|
|
512
|
+
"content",
|
|
513
|
+
JSONArray().put(
|
|
514
|
+
JSONObject()
|
|
515
|
+
.put("type", "mention")
|
|
516
|
+
.put("attrs", attrs)
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
val updateJson = editorInsertContentJsonAtSelectionScalar(
|
|
521
|
+
richTextView.editorId.toULong(),
|
|
522
|
+
queryState.anchor.toUInt(),
|
|
523
|
+
queryState.head.toUInt(),
|
|
524
|
+
docJson.toString()
|
|
525
|
+
)
|
|
526
|
+
richTextView.editorEditText.applyUpdateJSON(updateJson)
|
|
527
|
+
emitMentionSelect(mentions.trigger, suggestion)
|
|
528
|
+
lastMentionEventJson = null
|
|
529
|
+
clearMentionQueryState()
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private fun refreshToolbarStateFromEditorSelection() {
|
|
533
|
+
if (richTextView.editorId == 0L) return
|
|
534
|
+
val state = NativeToolbarState.fromUpdateJson(
|
|
535
|
+
editorGetCurrentState(richTextView.editorId.toULong())
|
|
536
|
+
) ?: return
|
|
537
|
+
toolbarState = state
|
|
538
|
+
keyboardToolbarView.applyState(state)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private fun ensureKeyboardToolbarAttached() {
|
|
542
|
+
val host = resolveActivity(context)?.findViewById<ViewGroup>(android.R.id.content) ?: return
|
|
543
|
+
if (keyboardToolbarView.parent === host) {
|
|
544
|
+
updateKeyboardToolbarLayout()
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
detachKeyboardToolbarIfNeeded()
|
|
548
|
+
host.addView(
|
|
549
|
+
keyboardToolbarView,
|
|
550
|
+
FrameLayout.LayoutParams(
|
|
551
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
552
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
553
|
+
Gravity.BOTTOM or Gravity.START
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
updateKeyboardToolbarLayout()
|
|
557
|
+
ViewCompat.requestApplyInsets(keyboardToolbarView)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private fun detachKeyboardToolbarIfNeeded() {
|
|
561
|
+
(keyboardToolbarView.parent as? ViewGroup)?.removeView(keyboardToolbarView)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private fun updateKeyboardToolbarLayout() {
|
|
565
|
+
val params = keyboardToolbarView.layoutParams as? FrameLayout.LayoutParams ?: return
|
|
566
|
+
val toolbarTheme = richTextView.editorEditText.theme?.toolbar
|
|
567
|
+
val density = resources.displayMetrics.density
|
|
568
|
+
params.gravity = Gravity.BOTTOM or Gravity.START
|
|
569
|
+
val horizontalInsetPx = ((toolbarTheme?.horizontalInset ?: 0f) * density).toInt()
|
|
570
|
+
val keyboardOffsetPx = ((toolbarTheme?.keyboardOffset ?: 0f) * density).toInt()
|
|
571
|
+
params.leftMargin = horizontalInsetPx
|
|
572
|
+
params.rightMargin = horizontalInsetPx
|
|
573
|
+
params.bottomMargin = currentImeBottom + keyboardOffsetPx
|
|
574
|
+
keyboardToolbarView.layoutParams = params
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private fun updateKeyboardToolbarVisibility() {
|
|
578
|
+
val shouldAttach =
|
|
579
|
+
showsToolbar &&
|
|
580
|
+
toolbarPlacement == ToolbarPlacement.KEYBOARD &&
|
|
581
|
+
richTextView.editorEditText.isEditable &&
|
|
582
|
+
richTextView.editorEditText.hasFocus()
|
|
583
|
+
|
|
584
|
+
if (!shouldAttach) {
|
|
585
|
+
keyboardToolbarView.visibility = View.GONE
|
|
586
|
+
detachKeyboardToolbarIfNeeded()
|
|
587
|
+
updateEditorViewportInset()
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
ensureKeyboardToolbarAttached()
|
|
592
|
+
keyboardToolbarView.visibility = if (currentImeBottom > 0) View.VISIBLE else View.INVISIBLE
|
|
593
|
+
updateEditorViewportInset()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private fun updateEditorViewportInset() {
|
|
597
|
+
val shouldReserveToolbarSpace =
|
|
598
|
+
heightBehavior == EditorHeightBehavior.FIXED &&
|
|
599
|
+
showsToolbar &&
|
|
600
|
+
toolbarPlacement == ToolbarPlacement.KEYBOARD &&
|
|
601
|
+
richTextView.editorEditText.isEditable &&
|
|
602
|
+
richTextView.editorEditText.hasFocus() &&
|
|
603
|
+
currentImeBottom > 0
|
|
604
|
+
|
|
605
|
+
if (!shouldReserveToolbarSpace) {
|
|
606
|
+
richTextView.setViewportBottomInsetPx(0)
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
val hostWidth = (resolveActivity(context)?.findViewById<ViewGroup>(android.R.id.content)?.width ?: width)
|
|
611
|
+
.coerceAtLeast(0)
|
|
612
|
+
val toolbarTheme = richTextView.editorEditText.theme?.toolbar
|
|
613
|
+
val density = resources.displayMetrics.density
|
|
614
|
+
val horizontalInsetPx = ((toolbarTheme?.horizontalInset ?: 0f) * density).toInt()
|
|
615
|
+
if (keyboardToolbarView.measuredHeight == 0) {
|
|
616
|
+
val availableWidth = (hostWidth - horizontalInsetPx * 2).coerceAtLeast(0)
|
|
617
|
+
val widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST)
|
|
618
|
+
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
619
|
+
keyboardToolbarView.measure(widthSpec, heightSpec)
|
|
620
|
+
}
|
|
621
|
+
val toolbarHeight = keyboardToolbarView.measuredHeight.coerceAtLeast(keyboardToolbarView.height)
|
|
622
|
+
richTextView.setViewportBottomInsetPx(toolbarHeight.coerceAtLeast(0))
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private fun handleListToggle(listType: String) {
|
|
626
|
+
val isActive = toolbarState.nodes[listType] == true
|
|
627
|
+
richTextView.editorEditText.performToolbarToggleList(listType, isActive)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private fun handleToolbarItemPress(item: NativeToolbarItem) {
|
|
631
|
+
when (item.type) {
|
|
632
|
+
ToolbarItemKind.mark -> item.mark?.let { richTextView.editorEditText.performToolbarToggleMark(it) }
|
|
633
|
+
ToolbarItemKind.list -> item.listType?.name?.let { handleListToggle(it) }
|
|
634
|
+
ToolbarItemKind.command -> when (item.command) {
|
|
635
|
+
ToolbarCommand.indentList -> richTextView.editorEditText.performToolbarIndentListItem()
|
|
636
|
+
ToolbarCommand.outdentList -> richTextView.editorEditText.performToolbarOutdentListItem()
|
|
637
|
+
ToolbarCommand.undo -> richTextView.editorEditText.performToolbarUndo()
|
|
638
|
+
ToolbarCommand.redo -> richTextView.editorEditText.performToolbarRedo()
|
|
639
|
+
null -> Unit
|
|
640
|
+
}
|
|
641
|
+
ToolbarItemKind.node -> item.nodeType?.let { richTextView.editorEditText.performToolbarInsertNode(it) }
|
|
642
|
+
ToolbarItemKind.action -> item.key?.let { onToolbarAction(mapOf("key" to it)) }
|
|
643
|
+
ToolbarItemKind.separator -> Unit
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
}
|