@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.
Files changed (47) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +143 -0
  3. package/android/build.gradle +39 -0
  4. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  5. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  6. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +131 -0
  7. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
  8. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  9. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
  10. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
  11. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
  12. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
  13. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
  14. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
  15. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
  16. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
  17. package/expo-module.config.json +9 -0
  18. package/ios/EditorAddons.swift +228 -0
  19. package/ios/EditorCore.xcframework/Info.plist +44 -0
  20. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  21. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  22. package/ios/EditorLayoutManager.swift +254 -0
  23. package/ios/EditorTheme.swift +372 -0
  24. package/ios/Generated_editor_core.swift +1143 -0
  25. package/ios/NativeEditorExpoView.swift +1417 -0
  26. package/ios/NativeEditorModule.swift +263 -0
  27. package/ios/PositionBridge.swift +278 -0
  28. package/ios/ReactNativeProseEditor.podspec +49 -0
  29. package/ios/RenderBridge.swift +825 -0
  30. package/ios/RichTextEditorView.swift +1559 -0
  31. package/ios/editor_coreFFI/editor_coreFFI.h +1014 -0
  32. package/ios/editor_coreFFI/module.modulemap +7 -0
  33. package/ios/editor_coreFFI.h +904 -0
  34. package/ios/editor_coreFFI.modulemap +7 -0
  35. package/package.json +66 -0
  36. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  37. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  38. package/rust/android/x86_64/libeditor_core.so +0 -0
  39. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2014 -0
  40. package/src/EditorTheme.ts +130 -0
  41. package/src/EditorToolbar.tsx +620 -0
  42. package/src/NativeEditorBridge.ts +607 -0
  43. package/src/NativeRichTextEditor.tsx +951 -0
  44. package/src/addons.ts +158 -0
  45. package/src/index.ts +63 -0
  46. package/src/schemas.ts +153 -0
  47. 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
+ }