@apollohg/react-native-prose-editor 0.5.20 → 0.5.21
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 +23 -1
- package/android/build.gradle +5 -6
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +167 -16
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +539 -73
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +14 -0
- package/app.plugin.js +62 -0
- package/dist/EditorToolbar.d.ts +3 -2
- package/dist/EditorToolbar.js +41 -13
- package/dist/NativeRichTextEditor.d.ts +9 -0
- package/dist/NativeRichTextEditor.js +252 -81
- package/dist/YjsCollaboration.d.ts +5 -0
- package/dist/YjsCollaboration.js +44 -8
- package/dist/index.d.ts +1 -1
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- 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/NativeEditorExpoView.swift +49 -2
- package/package.json +5 -2
- 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
|
@@ -12,7 +12,9 @@ import android.util.Log
|
|
|
12
12
|
import android.view.Gravity
|
|
13
13
|
import android.view.MotionEvent
|
|
14
14
|
import android.view.View
|
|
15
|
+
import android.view.ViewConfiguration
|
|
15
16
|
import android.view.ViewGroup
|
|
17
|
+
import android.view.ViewTreeObserver
|
|
16
18
|
import android.view.Window
|
|
17
19
|
import android.view.inputmethod.InputMethodManager
|
|
18
20
|
import android.widget.FrameLayout
|
|
@@ -32,6 +34,13 @@ import java.util.concurrent.atomic.AtomicReference
|
|
|
32
34
|
import uniffi.editor_core.*
|
|
33
35
|
|
|
34
36
|
private const val DESTROY_INVALIDATION_AWAIT_TIMEOUT_MS = 250L
|
|
37
|
+
private const val OUTSIDE_TAP_GESTURE_CONFIRM_DELAY_MS = 150L
|
|
38
|
+
|
|
39
|
+
internal enum class NativeEditorOutsideTapDecision {
|
|
40
|
+
IGNORE,
|
|
41
|
+
PRESERVE_FOCUS,
|
|
42
|
+
OUTSIDE_EDITOR
|
|
43
|
+
}
|
|
35
44
|
|
|
36
45
|
private class WeakNativeEditorExpoView private constructor(
|
|
37
46
|
val view: WeakReference<NativeEditorExpoView?>
|
|
@@ -258,44 +267,80 @@ internal object NativeEditorViewRegistry {
|
|
|
258
267
|
}
|
|
259
268
|
|
|
260
269
|
private object NativeEditorOutsideTapDispatcher {
|
|
261
|
-
private val dispatchers = WeakHashMap<Window,
|
|
270
|
+
private val dispatchers = WeakHashMap<Window, OutsideTapTouchDispatcher>()
|
|
262
271
|
|
|
263
|
-
fun register(window: Window, view: NativeEditorExpoView) {
|
|
264
|
-
val
|
|
272
|
+
fun register(window: Window, view: NativeEditorExpoView): Boolean {
|
|
273
|
+
val host = contentRootFor(window)
|
|
274
|
+
if (host == null) {
|
|
275
|
+
view.traceOutsideTap("register skipped missing content root")
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
265
278
|
val previousDispatcher = dispatchers[window]
|
|
266
|
-
val dispatcher = if (
|
|
279
|
+
val dispatcher = if (previousDispatcher?.host === host) {
|
|
267
280
|
previousDispatcher
|
|
268
|
-
?.takeIf { it !== currentCallback }
|
|
269
|
-
?.transferViewsTo(currentCallback)
|
|
270
|
-
currentCallback
|
|
271
281
|
} else {
|
|
272
|
-
|
|
282
|
+
OutsideTapTouchDispatcher(host).also { nextDispatcher ->
|
|
273
283
|
previousDispatcher?.transferViewsTo(nextDispatcher)
|
|
274
|
-
|
|
284
|
+
previousDispatcher?.detach()
|
|
285
|
+
dispatchers[window] = nextDispatcher
|
|
275
286
|
}
|
|
276
287
|
}
|
|
277
288
|
dispatchers[window] = dispatcher
|
|
278
289
|
dispatcher.add(view)
|
|
290
|
+
view.traceOutsideTap(
|
|
291
|
+
"register overlayAttached=${dispatcher.isAttached()} " +
|
|
292
|
+
"host=${host.javaClass.name} " +
|
|
293
|
+
"activeViews=${dispatcher.liveViews().size}"
|
|
294
|
+
)
|
|
295
|
+
return dispatcher.isAttached()
|
|
279
296
|
}
|
|
280
297
|
|
|
281
298
|
fun unregister(window: Window, view: NativeEditorExpoView) {
|
|
282
299
|
val dispatcher = dispatchers[window] ?: return
|
|
283
300
|
if (!dispatcher.remove(view)) return
|
|
301
|
+
dispatcher.detach()
|
|
284
302
|
dispatchers.remove(window)
|
|
285
|
-
if (window.callback === dispatcher) {
|
|
286
|
-
window.callback = dispatcher.baseCallback
|
|
287
|
-
}
|
|
288
303
|
}
|
|
289
304
|
|
|
290
|
-
private
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
305
|
+
private fun contentRootFor(window: Window): ViewGroup? {
|
|
306
|
+
val decorView = window.decorView
|
|
307
|
+
return decorView.findViewById<View>(android.R.id.content) as? ViewGroup
|
|
308
|
+
?: decorView as? ViewGroup
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private class OutsideTapTouchDispatcher(
|
|
312
|
+
val host: ViewGroup
|
|
313
|
+
) : View.OnTouchListener {
|
|
314
|
+
private data class OutsideTapCandidate(
|
|
315
|
+
val view: WeakReference<NativeEditorExpoView>,
|
|
316
|
+
val downRawX: Float,
|
|
317
|
+
val downRawY: Float,
|
|
318
|
+
val editorRectOnDown: Rect?,
|
|
319
|
+
val confirm: Runnable
|
|
320
|
+
)
|
|
321
|
+
|
|
294
322
|
private val views = mutableListOf<WeakReference<NativeEditorExpoView>>()
|
|
295
|
-
private
|
|
323
|
+
private val pendingOutsideTapCandidates = mutableListOf<OutsideTapCandidate>()
|
|
324
|
+
private val touchSlopPx = ViewConfiguration.get(host.context).scaledTouchSlop
|
|
325
|
+
private val scrollChangedListener = ViewTreeObserver.OnScrollChangedListener {
|
|
326
|
+
cancelPendingOutsideTapCandidates("scroll")
|
|
327
|
+
}
|
|
328
|
+
private var scrollListenerTreeObserver: ViewTreeObserver? = null
|
|
329
|
+
private val observerView = View(host.context).apply {
|
|
330
|
+
isClickable = false
|
|
331
|
+
isFocusable = false
|
|
332
|
+
isFocusableInTouchMode = false
|
|
333
|
+
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
init {
|
|
337
|
+
observerView.setOnTouchListener(this)
|
|
338
|
+
attach()
|
|
339
|
+
}
|
|
296
340
|
|
|
297
341
|
fun add(view: NativeEditorExpoView) {
|
|
298
342
|
prune()
|
|
343
|
+
attach()
|
|
299
344
|
if (views.any { it.get() === view }) return
|
|
300
345
|
views.add(WeakReference(view))
|
|
301
346
|
}
|
|
@@ -305,48 +350,201 @@ private object NativeEditorOutsideTapDispatcher {
|
|
|
305
350
|
return views.mapNotNull { it.get() }
|
|
306
351
|
}
|
|
307
352
|
|
|
308
|
-
fun transferViewsTo(target:
|
|
353
|
+
fun transferViewsTo(target: OutsideTapTouchDispatcher) {
|
|
309
354
|
liveViews().forEach { target.add(it) }
|
|
310
355
|
views.clear()
|
|
311
|
-
|
|
356
|
+
cancelPendingOutsideTapCandidates("transfer")
|
|
312
357
|
}
|
|
313
358
|
|
|
314
359
|
fun remove(view: NativeEditorExpoView): Boolean {
|
|
360
|
+
cancelPendingOutsideTapCandidatesFor(view, "remove view")
|
|
315
361
|
views.removeAll { it.get()?.let { candidate -> candidate === view } != false }
|
|
316
362
|
return views.isEmpty()
|
|
317
363
|
}
|
|
318
364
|
|
|
319
|
-
override fun
|
|
320
|
-
if (disabled) {
|
|
321
|
-
return baseCallback.dispatchTouchEvent(event)
|
|
322
|
-
}
|
|
365
|
+
override fun onTouch(view: View, event: MotionEvent): Boolean {
|
|
323
366
|
val activeViews = liveViews()
|
|
324
|
-
if (
|
|
325
|
-
return
|
|
367
|
+
if (activeViews.isEmpty()) {
|
|
368
|
+
return false
|
|
326
369
|
}
|
|
327
370
|
|
|
371
|
+
when (event.actionMasked) {
|
|
372
|
+
MotionEvent.ACTION_DOWN -> handleActionDown(activeViews, event)
|
|
373
|
+
MotionEvent.ACTION_MOVE -> {
|
|
374
|
+
if (hasMovedBeyondTapSlop(event)) {
|
|
375
|
+
cancelPendingOutsideTapCandidates("move")
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
MotionEvent.ACTION_UP -> {
|
|
379
|
+
if (hasMovedBeyondTapSlop(event)) {
|
|
380
|
+
cancelPendingOutsideTapCandidates("up moved")
|
|
381
|
+
} else {
|
|
382
|
+
confirmPendingOutsideTapCandidates("up")
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
MotionEvent.ACTION_CANCEL -> cancelPendingOutsideTapCandidates("cancel")
|
|
386
|
+
}
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private fun handleActionDown(activeViews: List<NativeEditorExpoView>, event: MotionEvent) {
|
|
391
|
+
cancelPendingOutsideTapCandidates("new down")
|
|
328
392
|
val decisions = activeViews.map { view ->
|
|
329
|
-
view to view.
|
|
393
|
+
view to view.prepareOutsideTapDecisionForWindowEvent(event)
|
|
330
394
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
395
|
+
decisions.forEach { (view, decision) ->
|
|
396
|
+
view.traceOutsideTap(
|
|
397
|
+
"dispatch overlay action=${event.action} raw=${event.rawX.toInt()},${event.rawY.toInt()} decision=$decision"
|
|
398
|
+
)
|
|
399
|
+
if (decision == NativeEditorOutsideTapDecision.OUTSIDE_EDITOR) {
|
|
400
|
+
scheduleOutsideTapCandidate(view, event)
|
|
335
401
|
} else {
|
|
336
|
-
view.
|
|
402
|
+
view.handleOutsideTapDecisionFromWindowDispatcher(decision)
|
|
337
403
|
}
|
|
338
404
|
}
|
|
339
|
-
return result
|
|
340
405
|
}
|
|
341
406
|
|
|
342
|
-
private fun
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
407
|
+
private fun scheduleOutsideTapCandidate(view: NativeEditorExpoView, event: MotionEvent) {
|
|
408
|
+
val editorRect = Rect()
|
|
409
|
+
val editorRectOnDown = if (
|
|
410
|
+
view.richTextView.editorEditText.getGlobalVisibleRect(editorRect) &&
|
|
411
|
+
!editorRect.isEmpty
|
|
412
|
+
) {
|
|
413
|
+
editorRect
|
|
414
|
+
} else {
|
|
415
|
+
null
|
|
416
|
+
}
|
|
417
|
+
val viewRef = WeakReference(view)
|
|
418
|
+
lateinit var candidate: OutsideTapCandidate
|
|
419
|
+
val confirm = Runnable {
|
|
420
|
+
confirmOutsideTapCandidate(candidate, "delay")
|
|
421
|
+
}
|
|
422
|
+
candidate = OutsideTapCandidate(
|
|
423
|
+
view = viewRef,
|
|
424
|
+
downRawX = event.rawX,
|
|
425
|
+
downRawY = event.rawY,
|
|
426
|
+
editorRectOnDown = editorRectOnDown,
|
|
427
|
+
confirm = confirm
|
|
428
|
+
)
|
|
429
|
+
pendingOutsideTapCandidates.add(candidate)
|
|
430
|
+
ensureScrollListener()
|
|
431
|
+
view.traceOutsideTap("candidate outside tap")
|
|
432
|
+
observerView.postDelayed(confirm, OUTSIDE_TAP_GESTURE_CONFIRM_DELAY_MS)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private fun confirmPendingOutsideTapCandidates(reason: String) {
|
|
436
|
+
val candidates = pendingOutsideTapCandidates.toList()
|
|
437
|
+
candidates.forEach { candidate ->
|
|
438
|
+
confirmOutsideTapCandidate(candidate, reason)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private fun confirmOutsideTapCandidate(candidate: OutsideTapCandidate, reason: String) {
|
|
443
|
+
if (!pendingOutsideTapCandidates.remove(candidate)) return
|
|
444
|
+
removeScrollListenerIfIdle()
|
|
445
|
+
observerView.removeCallbacks(candidate.confirm)
|
|
446
|
+
val view = candidate.view.get() ?: return
|
|
447
|
+
if (editorMovedBeyondTapSlop(view, candidate)) {
|
|
448
|
+
view.traceOutsideTap("cancel outside tap candidate reason=$reason moved")
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
view.traceOutsideTap("confirm outside tap candidate reason=$reason")
|
|
452
|
+
view.handleOutsideTapDecisionFromWindowDispatcher(NativeEditorOutsideTapDecision.OUTSIDE_EDITOR)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private fun hasMovedBeyondTapSlop(event: MotionEvent): Boolean =
|
|
456
|
+
pendingOutsideTapCandidates.any { candidate ->
|
|
457
|
+
val dx = event.rawX - candidate.downRawX
|
|
458
|
+
val dy = event.rawY - candidate.downRawY
|
|
459
|
+
dx * dx + dy * dy > touchSlopPx * touchSlopPx
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private fun editorMovedBeyondTapSlop(
|
|
463
|
+
view: NativeEditorExpoView,
|
|
464
|
+
candidate: OutsideTapCandidate
|
|
465
|
+
): Boolean {
|
|
466
|
+
val editorRectOnDown = candidate.editorRectOnDown ?: return false
|
|
467
|
+
val currentRect = Rect()
|
|
468
|
+
if (!view.richTextView.editorEditText.getGlobalVisibleRect(currentRect)) {
|
|
469
|
+
return true
|
|
470
|
+
}
|
|
471
|
+
val dx = currentRect.left - editorRectOnDown.left
|
|
472
|
+
val dy = currentRect.top - editorRectOnDown.top
|
|
473
|
+
return dx * dx + dy * dy > touchSlopPx * touchSlopPx
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private fun cancelPendingOutsideTapCandidatesFor(view: NativeEditorExpoView, reason: String) {
|
|
477
|
+
val candidates = pendingOutsideTapCandidates.toList()
|
|
478
|
+
candidates.forEach { candidate ->
|
|
479
|
+
if (candidate.view.get() === view) {
|
|
480
|
+
pendingOutsideTapCandidates.remove(candidate)
|
|
481
|
+
observerView.removeCallbacks(candidate.confirm)
|
|
482
|
+
view.traceOutsideTap("cancel outside tap candidate reason=$reason")
|
|
348
483
|
}
|
|
349
484
|
}
|
|
485
|
+
removeScrollListenerIfIdle()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private fun cancelPendingOutsideTapCandidates(reason: String) {
|
|
489
|
+
val candidates = pendingOutsideTapCandidates.toList()
|
|
490
|
+
pendingOutsideTapCandidates.clear()
|
|
491
|
+
removeScrollListener()
|
|
492
|
+
candidates.forEach { candidate ->
|
|
493
|
+
observerView.removeCallbacks(candidate.confirm)
|
|
494
|
+
candidate.view.get()?.traceOutsideTap("cancel outside tap candidate reason=$reason")
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private fun ensureScrollListener() {
|
|
499
|
+
val activeObserver = scrollListenerTreeObserver
|
|
500
|
+
if (activeObserver?.isAlive == true && activeObserver === host.viewTreeObserver) {
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
removeScrollListener()
|
|
504
|
+
val nextObserver = host.viewTreeObserver
|
|
505
|
+
if (nextObserver.isAlive) {
|
|
506
|
+
nextObserver.addOnScrollChangedListener(scrollChangedListener)
|
|
507
|
+
scrollListenerTreeObserver = nextObserver
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private fun removeScrollListenerIfIdle() {
|
|
512
|
+
if (pendingOutsideTapCandidates.isEmpty()) {
|
|
513
|
+
removeScrollListener()
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private fun removeScrollListener() {
|
|
518
|
+
val observer = scrollListenerTreeObserver
|
|
519
|
+
if (observer?.isAlive == true) {
|
|
520
|
+
observer.removeOnScrollChangedListener(scrollChangedListener)
|
|
521
|
+
}
|
|
522
|
+
scrollListenerTreeObserver = null
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
fun isAttached(): Boolean = observerView.parent === host
|
|
526
|
+
|
|
527
|
+
fun detach() {
|
|
528
|
+
cancelPendingOutsideTapCandidates("detach")
|
|
529
|
+
(observerView.parent as? ViewGroup)?.removeView(observerView)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private fun attach() {
|
|
533
|
+
if (observerView.parent !== host) {
|
|
534
|
+
detach()
|
|
535
|
+
host.addView(
|
|
536
|
+
observerView,
|
|
537
|
+
ViewGroup.LayoutParams(
|
|
538
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
539
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
observerView.bringToFront()
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private fun prune() {
|
|
547
|
+
views.removeAll { it.get() == null }
|
|
350
548
|
}
|
|
351
549
|
}
|
|
352
550
|
}
|
|
@@ -415,8 +613,12 @@ class NativeEditorExpoView(
|
|
|
415
613
|
internal var blockThemePreflightForTesting = false
|
|
416
614
|
internal var onToolbarActionForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
417
615
|
internal var onAddonEventForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
616
|
+
internal var onSelectionChangeForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
418
617
|
internal var onFocusChangeForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
618
|
+
internal var onContentHeightChangeForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
619
|
+
internal var onEditorUpdateForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
419
620
|
internal var onEditorReadyForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
621
|
+
internal var onOutsideTapTraceForTesting: ((String) -> Unit)? = null
|
|
420
622
|
internal var onRefreshToolbarStateFromEditorSelectionForTesting: (() -> String?)? = null
|
|
421
623
|
internal var onBeforePrepareForEditorCommandForTesting: (() -> Unit)? = null
|
|
422
624
|
private var isAttachedToNativeWindow = false
|
|
@@ -424,8 +626,10 @@ class NativeEditorExpoView(
|
|
|
424
626
|
private var heightBehavior = EditorHeightBehavior.FIXED
|
|
425
627
|
private var lastEmittedContentHeight = 0
|
|
426
628
|
private var outsideTapWindow: Window? = null
|
|
629
|
+
private var pendingOutsideTapHandlerInstallRetry: Runnable? = null
|
|
427
630
|
private var toolbarFramesInWindow: List<RectF> = emptyList()
|
|
428
631
|
private var lastToolbarTouchUptimeMs: Long? = null
|
|
632
|
+
private var editorFocusedForOutsideTapOverrideForTesting: Boolean? = null
|
|
429
633
|
private var pendingOutsideTapBlur: Runnable? = null
|
|
430
634
|
private var pendingKeyboardDismiss: Runnable? = null
|
|
431
635
|
private var pendingToolbarRefocus: Runnable? = null
|
|
@@ -456,6 +660,12 @@ class NativeEditorExpoView(
|
|
|
456
660
|
private var pendingEditorUpdateEditorId: Long? = null
|
|
457
661
|
private var pendingEditorUpdateRevision = 0
|
|
458
662
|
private var appliedEditorUpdateRevision = 0
|
|
663
|
+
private var pendingEditorResetUpdateJson: String? = null
|
|
664
|
+
private var pendingEditorResetUpdateEditorId: Long? = null
|
|
665
|
+
private var pendingEditorResetUpdateRevision = 0
|
|
666
|
+
private var appliedEditorResetUpdateRevision = 0
|
|
667
|
+
private var lastEditorResetUpdateJsonProp: String? = null
|
|
668
|
+
private var lastEditorResetUpdateEditorIdProp: Long? = null
|
|
459
669
|
private var pendingEditorUpdateRetryScheduled = false
|
|
460
670
|
private var pendingEditorUpdateRetryEditorId: Long? = null
|
|
461
671
|
private var pendingEditorUpdateRetryGeneration = 0
|
|
@@ -512,9 +722,10 @@ class NativeEditorExpoView(
|
|
|
512
722
|
if (hasFocus) {
|
|
513
723
|
cancelPendingToolbarRefocus()
|
|
514
724
|
installOutsideTapBlurHandlerIfNeeded()
|
|
725
|
+
scheduleOutsideTapBlurHandlerInstallRetry()
|
|
515
726
|
refreshMentionQuery()
|
|
516
727
|
} else {
|
|
517
|
-
if (
|
|
728
|
+
if (consumeToolbarFocusPreservationForBlur()) {
|
|
518
729
|
scheduleToolbarRefocus()
|
|
519
730
|
return@setOnFocusChangeListener
|
|
520
731
|
}
|
|
@@ -543,6 +754,7 @@ class NativeEditorExpoView(
|
|
|
543
754
|
handleEditorDestroyed(id)
|
|
544
755
|
return
|
|
545
756
|
}
|
|
757
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
546
758
|
applyPendingEditorUpdateIfNeeded()
|
|
547
759
|
applyPendingThemeIfNeeded()
|
|
548
760
|
refreshReadyStateIfSettled()
|
|
@@ -564,7 +776,11 @@ class NativeEditorExpoView(
|
|
|
564
776
|
if (pendingEditorUpdateEditorId != null && pendingEditorUpdateEditorId != id) {
|
|
565
777
|
clearPendingEditorUpdateState()
|
|
566
778
|
}
|
|
779
|
+
if (pendingEditorResetUpdateEditorId != null && pendingEditorResetUpdateEditorId != id) {
|
|
780
|
+
clearPendingEditorResetUpdateState()
|
|
781
|
+
}
|
|
567
782
|
appliedEditorUpdateRevision = 0
|
|
783
|
+
appliedEditorResetUpdateRevision = 0
|
|
568
784
|
clearPendingViewCommandUpdateRetry()
|
|
569
785
|
cancelPendingThemeRetry()
|
|
570
786
|
if (hasPendingTheme) {
|
|
@@ -590,7 +806,7 @@ class NativeEditorExpoView(
|
|
|
590
806
|
return
|
|
591
807
|
}
|
|
592
808
|
|
|
593
|
-
if (hasPendingEditorUpdateForEditor(id)) {
|
|
809
|
+
if (hasPendingEditorResetUpdateForEditor(id) || hasPendingEditorUpdateForEditor(id)) {
|
|
594
810
|
richTextView.setEditorIdWhileDetached(id)
|
|
595
811
|
richTextView.rebindEditorIfNeeded(notifyListener = false)
|
|
596
812
|
} else {
|
|
@@ -605,6 +821,7 @@ class NativeEditorExpoView(
|
|
|
605
821
|
toolbarState = NativeToolbarState.empty
|
|
606
822
|
keyboardToolbarView.applyState(toolbarState)
|
|
607
823
|
}
|
|
824
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
608
825
|
applyPendingEditorUpdateIfNeeded()
|
|
609
826
|
applyPendingThemeIfNeeded()
|
|
610
827
|
refreshReadyStateIfSettled()
|
|
@@ -807,15 +1024,48 @@ class NativeEditorExpoView(
|
|
|
807
1024
|
pendingEditorUpdateRevision = editorUpdateRevision
|
|
808
1025
|
}
|
|
809
1026
|
|
|
1027
|
+
fun setPendingEditorResetUpdateJson(editorResetUpdateJson: String?) {
|
|
1028
|
+
lastEditorResetUpdateJsonProp = editorResetUpdateJson
|
|
1029
|
+
pendingEditorResetUpdateJson = editorResetUpdateJson
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
fun setPendingEditorResetUpdateEditorId(editorResetUpdateEditorId: Long?) {
|
|
1033
|
+
lastEditorResetUpdateEditorIdProp = editorResetUpdateEditorId
|
|
1034
|
+
pendingEditorResetUpdateEditorId = editorResetUpdateEditorId
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
fun setPendingEditorResetUpdateRevision(editorResetUpdateRevision: Int) {
|
|
1038
|
+
if (pendingEditorResetUpdateRevision != editorResetUpdateRevision) {
|
|
1039
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
1040
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
1041
|
+
}
|
|
1042
|
+
if (editorResetUpdateRevision != 0 && pendingEditorResetUpdateJson == null) {
|
|
1043
|
+
pendingEditorResetUpdateJson = lastEditorResetUpdateJsonProp
|
|
1044
|
+
}
|
|
1045
|
+
if (editorResetUpdateRevision != 0 && pendingEditorResetUpdateEditorId == null) {
|
|
1046
|
+
pendingEditorResetUpdateEditorId = lastEditorResetUpdateEditorIdProp
|
|
1047
|
+
}
|
|
1048
|
+
pendingEditorResetUpdateRevision = editorResetUpdateRevision
|
|
1049
|
+
}
|
|
1050
|
+
|
|
810
1051
|
private fun hasPendingEditorUpdateForEditor(editorId: Long): Boolean =
|
|
811
1052
|
pendingEditorUpdateJson != null &&
|
|
812
1053
|
pendingEditorUpdateRevision != 0 &&
|
|
813
1054
|
pendingEditorUpdateRevision != appliedEditorUpdateRevision &&
|
|
814
1055
|
pendingEditorUpdateEditorId == editorId
|
|
815
1056
|
|
|
1057
|
+
private fun hasPendingEditorResetUpdateForEditor(editorId: Long): Boolean =
|
|
1058
|
+
pendingEditorResetUpdateJson != null &&
|
|
1059
|
+
pendingEditorResetUpdateRevision != 0 &&
|
|
1060
|
+
pendingEditorResetUpdateRevision != appliedEditorResetUpdateRevision &&
|
|
1061
|
+
pendingEditorResetUpdateEditorId == editorId
|
|
1062
|
+
|
|
816
1063
|
private fun hasPendingEditorUpdateForCurrentEditor(): Boolean =
|
|
817
1064
|
hasPendingEditorUpdateForEditor(richTextView.editorId)
|
|
818
1065
|
|
|
1066
|
+
private fun hasPendingEditorResetUpdateForCurrentEditor(): Boolean =
|
|
1067
|
+
hasPendingEditorResetUpdateForEditor(richTextView.editorId)
|
|
1068
|
+
|
|
819
1069
|
private fun pendingEditorUpdateCommandPreparationJSON(): String =
|
|
820
1070
|
NativeEditorViewRegistry.commandPreparationJSON(
|
|
821
1071
|
ready = false,
|
|
@@ -823,10 +1073,11 @@ class NativeEditorExpoView(
|
|
|
823
1073
|
)
|
|
824
1074
|
|
|
825
1075
|
private fun shouldBlockEditorCommandForPendingUpdate(): Boolean =
|
|
826
|
-
hasPendingEditorUpdateForCurrentEditor()
|
|
1076
|
+
hasPendingEditorResetUpdateForCurrentEditor() || hasPendingEditorUpdateForCurrentEditor()
|
|
827
1077
|
|
|
828
1078
|
private fun refreshReadyStateIfSettled() {
|
|
829
1079
|
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1080
|
+
if (hasPendingEditorResetUpdateForCurrentEditor()) return
|
|
830
1081
|
if (hasPendingEditorUpdateForCurrentEditor()) return
|
|
831
1082
|
if (!isAttachedToNativeWindow) return
|
|
832
1083
|
if (richTextView.editorEditText.editorId != richTextView.editorId) return
|
|
@@ -835,6 +1086,54 @@ class NativeEditorExpoView(
|
|
|
835
1086
|
emitEditorReadyIfNeeded()
|
|
836
1087
|
}
|
|
837
1088
|
|
|
1089
|
+
fun applyPendingEditorResetUpdateIfNeeded() {
|
|
1090
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1091
|
+
if (pendingEditorResetUpdateRevision == 0) return
|
|
1092
|
+
val revision = pendingEditorResetUpdateRevision
|
|
1093
|
+
val editorId = richTextView.editorId
|
|
1094
|
+
val expectedEditorId = pendingEditorResetUpdateEditorId
|
|
1095
|
+
if (expectedEditorId == null) return
|
|
1096
|
+
if (expectedEditorId != editorId) return
|
|
1097
|
+
if (pendingEditorResetUpdateJson == null) {
|
|
1098
|
+
clearPendingEditorResetUpdateState(resetAppliedRevision = false)
|
|
1099
|
+
refreshReadyStateIfSettled()
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
val updateJson = pendingEditorResetUpdateJson ?: return
|
|
1103
|
+
if (revision == appliedEditorResetUpdateRevision) {
|
|
1104
|
+
clearPendingEditorResetUpdateState(resetAppliedRevision = false)
|
|
1105
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1106
|
+
refreshReadyStateIfSettled()
|
|
1107
|
+
return
|
|
1108
|
+
}
|
|
1109
|
+
if (editorId != 0L && !isAttachedToNativeWindow) return
|
|
1110
|
+
val apply = Runnable {
|
|
1111
|
+
if (editorId != richTextView.editorId) return@Runnable
|
|
1112
|
+
if (expectedEditorId != richTextView.editorId) return@Runnable
|
|
1113
|
+
if (editorId != 0L && !isAttachedToNativeWindow) return@Runnable
|
|
1114
|
+
if (revision != pendingEditorResetUpdateRevision) return@Runnable
|
|
1115
|
+
if (revision == appliedEditorResetUpdateRevision) {
|
|
1116
|
+
clearPendingEditorResetUpdateState(resetAppliedRevision = false)
|
|
1117
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1118
|
+
refreshReadyStateIfSettled()
|
|
1119
|
+
return@Runnable
|
|
1120
|
+
}
|
|
1121
|
+
if (applyEditorResetUpdate(updateJson)) {
|
|
1122
|
+
appliedEditorResetUpdateRevision = revision
|
|
1123
|
+
clearPendingEditorResetUpdateState(resetAppliedRevision = false)
|
|
1124
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1125
|
+
refreshReadyStateIfSettled()
|
|
1126
|
+
} else {
|
|
1127
|
+
schedulePendingEditorUpdateRetry()
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
1131
|
+
apply.run()
|
|
1132
|
+
} else if (!post(apply)) {
|
|
1133
|
+
richTextView.post(apply)
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
838
1137
|
fun applyPendingEditorUpdateIfNeeded() {
|
|
839
1138
|
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
840
1139
|
if (pendingEditorUpdateRevision == 0) return
|
|
@@ -898,6 +1197,15 @@ class NativeEditorExpoView(
|
|
|
898
1197
|
cancelPendingEditorUpdateRetry()
|
|
899
1198
|
}
|
|
900
1199
|
|
|
1200
|
+
private fun clearPendingEditorResetUpdateState(resetAppliedRevision: Boolean = true) {
|
|
1201
|
+
pendingEditorResetUpdateJson = null
|
|
1202
|
+
pendingEditorResetUpdateEditorId = null
|
|
1203
|
+
pendingEditorResetUpdateRevision = 0
|
|
1204
|
+
if (resetAppliedRevision) {
|
|
1205
|
+
appliedEditorResetUpdateRevision = 0
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
901
1209
|
private fun cancelPendingEditorUpdateRetry() {
|
|
902
1210
|
pendingEditorUpdateRetryScheduled = false
|
|
903
1211
|
pendingEditorUpdateRetryEditorId = null
|
|
@@ -939,6 +1247,7 @@ class NativeEditorExpoView(
|
|
|
939
1247
|
}
|
|
940
1248
|
pendingEditorUpdateRetryScheduled = false
|
|
941
1249
|
pendingEditorUpdateRetryEditorId = null
|
|
1250
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
942
1251
|
applyPendingEditorUpdateIfNeeded()
|
|
943
1252
|
}
|
|
944
1253
|
mainHandler.postDelayed(retry, delayMs)
|
|
@@ -1065,6 +1374,9 @@ class NativeEditorExpoView(
|
|
|
1065
1374
|
return
|
|
1066
1375
|
}
|
|
1067
1376
|
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1377
|
+
if (pendingEditorResetUpdateJson != null) {
|
|
1378
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
1379
|
+
}
|
|
1068
1380
|
if (pendingEditorUpdateJson != null) {
|
|
1069
1381
|
pendingEditorUpdateRetryAttempts = 0
|
|
1070
1382
|
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
@@ -1277,8 +1589,14 @@ class NativeEditorExpoView(
|
|
|
1277
1589
|
}
|
|
1278
1590
|
|
|
1279
1591
|
fun focus() {
|
|
1592
|
+
focusInternal(cancelPendingOutsideTapBlur = true)
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
private fun focusInternal(cancelPendingOutsideTapBlur: Boolean) {
|
|
1280
1596
|
if (!canFocusCurrentEditor()) return
|
|
1281
|
-
cancelPendingOutsideTapBlur
|
|
1597
|
+
if (cancelPendingOutsideTapBlur) {
|
|
1598
|
+
cancelPendingOutsideTapBlur()
|
|
1599
|
+
}
|
|
1282
1600
|
cancelPendingKeyboardDismiss()
|
|
1283
1601
|
cancelPendingBlurRetry()
|
|
1284
1602
|
richTextView.editorEditText.requestFocus()
|
|
@@ -1292,6 +1610,7 @@ class NativeEditorExpoView(
|
|
|
1292
1610
|
fun blur() {
|
|
1293
1611
|
cancelPendingOutsideTapBlur()
|
|
1294
1612
|
cancelPendingKeyboardDismiss()
|
|
1613
|
+
cancelPendingToolbarRefocus()
|
|
1295
1614
|
clearRecentToolbarTouch()
|
|
1296
1615
|
performBlur(deferKeyboardDismiss = false, allowRetry = true)
|
|
1297
1616
|
}
|
|
@@ -1311,7 +1630,11 @@ class NativeEditorExpoView(
|
|
|
1311
1630
|
|
|
1312
1631
|
private fun completeBlur(deferKeyboardDismiss: Boolean) {
|
|
1313
1632
|
cancelPendingBlurRetry()
|
|
1633
|
+
traceOutsideTap(
|
|
1634
|
+
"complete blur deferKeyboardDismiss=$deferKeyboardDismiss focusedBefore=${richTextView.editorEditText.hasFocus()}"
|
|
1635
|
+
)
|
|
1314
1636
|
richTextView.editorEditText.clearFocus()
|
|
1637
|
+
traceOutsideTap("complete blur focusedAfter=${richTextView.editorEditText.hasFocus()}")
|
|
1315
1638
|
if (deferKeyboardDismiss) {
|
|
1316
1639
|
val dismiss = Runnable {
|
|
1317
1640
|
pendingKeyboardDismiss = null
|
|
@@ -1355,6 +1678,7 @@ class NativeEditorExpoView(
|
|
|
1355
1678
|
|
|
1356
1679
|
private fun blurWithDeferredKeyboardDismiss() {
|
|
1357
1680
|
cancelPendingKeyboardDismiss()
|
|
1681
|
+
cancelPendingToolbarRefocus()
|
|
1358
1682
|
clearRecentToolbarTouch()
|
|
1359
1683
|
performBlur(deferKeyboardDismiss = true, allowRetry = true)
|
|
1360
1684
|
}
|
|
@@ -1370,7 +1694,7 @@ class NativeEditorExpoView(
|
|
|
1370
1694
|
if (refocusGeneration != pendingToolbarRefocusGeneration) return@Runnable
|
|
1371
1695
|
if (pendingToolbarRefocusEditorId != richTextView.editorId) return@Runnable
|
|
1372
1696
|
pendingToolbarRefocusEditorId = null
|
|
1373
|
-
|
|
1697
|
+
focusInternal(cancelPendingOutsideTapBlur = false)
|
|
1374
1698
|
}
|
|
1375
1699
|
pendingToolbarRefocus = refocus
|
|
1376
1700
|
richTextView.editorEditText.post(refocus)
|
|
@@ -1387,8 +1711,10 @@ class NativeEditorExpoView(
|
|
|
1387
1711
|
|
|
1388
1712
|
private fun scheduleOutsideTapBlur() {
|
|
1389
1713
|
cancelPendingOutsideTapBlur()
|
|
1714
|
+
traceOutsideTap("schedule outside blur focused=${richTextView.editorEditText.hasFocus()}")
|
|
1390
1715
|
val blur = Runnable {
|
|
1391
1716
|
pendingOutsideTapBlur = null
|
|
1717
|
+
traceOutsideTap("run outside blur focused=${richTextView.editorEditText.hasFocus()}")
|
|
1392
1718
|
if (richTextView.editorEditText.hasFocus()) {
|
|
1393
1719
|
blurWithDeferredKeyboardDismiss()
|
|
1394
1720
|
}
|
|
@@ -1399,6 +1725,7 @@ class NativeEditorExpoView(
|
|
|
1399
1725
|
|
|
1400
1726
|
private fun cancelPendingOutsideTapBlur() {
|
|
1401
1727
|
pendingOutsideTapBlur?.let {
|
|
1728
|
+
traceOutsideTap("cancel outside blur")
|
|
1402
1729
|
richTextView.editorEditText.removeCallbacks(it)
|
|
1403
1730
|
pendingOutsideTapBlur = null
|
|
1404
1731
|
}
|
|
@@ -1469,6 +1796,12 @@ class NativeEditorExpoView(
|
|
|
1469
1796
|
pendingEditorUpdateEditorId = null
|
|
1470
1797
|
pendingEditorUpdateRevision = 0
|
|
1471
1798
|
appliedEditorUpdateRevision = 0
|
|
1799
|
+
pendingEditorResetUpdateJson = null
|
|
1800
|
+
pendingEditorResetUpdateEditorId = null
|
|
1801
|
+
pendingEditorResetUpdateRevision = 0
|
|
1802
|
+
appliedEditorResetUpdateRevision = 0
|
|
1803
|
+
lastEditorResetUpdateJsonProp = null
|
|
1804
|
+
lastEditorResetUpdateEditorIdProp = null
|
|
1472
1805
|
lastDocumentVersion = null
|
|
1473
1806
|
lastReadyEditorId = null
|
|
1474
1807
|
toolbarState = NativeToolbarState.empty
|
|
@@ -1501,11 +1834,13 @@ class NativeEditorExpoView(
|
|
|
1501
1834
|
return
|
|
1502
1835
|
}
|
|
1503
1836
|
richTextView.rebindEditorIfNeeded(
|
|
1504
|
-
notifyListener = !
|
|
1837
|
+
notifyListener = !hasPendingEditorResetUpdateForEditor(editorId) &&
|
|
1838
|
+
!hasPendingEditorUpdateForEditor(editorId)
|
|
1505
1839
|
)
|
|
1506
1840
|
if (hasPendingTheme) {
|
|
1507
1841
|
pendingThemeRetryEditorId = editorId
|
|
1508
1842
|
}
|
|
1843
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
1509
1844
|
applyPendingEditorUpdateIfNeeded()
|
|
1510
1845
|
applyPendingThemeIfNeeded()
|
|
1511
1846
|
refreshReadyStateIfSettled()
|
|
@@ -1517,6 +1852,7 @@ class NativeEditorExpoView(
|
|
|
1517
1852
|
if (editorId == 0L) return false
|
|
1518
1853
|
if (!isAttachedToNativeWindow) return false
|
|
1519
1854
|
if (richTextView.editorEditText.editorId != editorId) return false
|
|
1855
|
+
if (hasPendingEditorResetUpdateForCurrentEditor()) return false
|
|
1520
1856
|
if (hasPendingEditorUpdateForCurrentEditor()) return false
|
|
1521
1857
|
lastReadyEditorId = editorId
|
|
1522
1858
|
val payload = mutableMapOf<String, Any>("editorId" to editorId)
|
|
@@ -1688,18 +2024,58 @@ class NativeEditorExpoView(
|
|
|
1688
2024
|
if (contentHeight <= 0) return
|
|
1689
2025
|
if (!force && contentHeight == lastEmittedContentHeight) return
|
|
1690
2026
|
lastEmittedContentHeight = contentHeight
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
"editorId" to richTextView.editorId
|
|
1695
|
-
)
|
|
2027
|
+
val event = mapOf(
|
|
2028
|
+
"contentHeight" to contentHeight,
|
|
2029
|
+
"editorId" to richTextView.editorId
|
|
1696
2030
|
)
|
|
2031
|
+
onContentHeightChangeForTesting?.invoke(event) ?: onContentHeightChange(event)
|
|
1697
2032
|
}
|
|
1698
2033
|
|
|
1699
2034
|
/** Applies an editor update from JS without echoing it back through events. */
|
|
1700
2035
|
fun applyEditorUpdate(updateJson: String): Boolean =
|
|
1701
2036
|
applyEditorUpdate(updateJson, scheduleViewCommandRetry = true)
|
|
1702
2037
|
|
|
2038
|
+
/** Applies a reset-style update from JS, discarding pending native composition. */
|
|
2039
|
+
fun applyEditorResetUpdate(updateJson: String): Boolean {
|
|
2040
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2041
|
+
val postedEditorId = richTextView.editorId
|
|
2042
|
+
val apply = Runnable {
|
|
2043
|
+
if (postedEditorId != richTextView.editorId) return@Runnable
|
|
2044
|
+
applyEditorResetUpdate(updateJson)
|
|
2045
|
+
}
|
|
2046
|
+
if (!post(apply)) {
|
|
2047
|
+
richTextView.post(apply)
|
|
2048
|
+
}
|
|
2049
|
+
return false
|
|
2050
|
+
}
|
|
2051
|
+
if (handleDestroyedCurrentEditorIfNeeded()) {
|
|
2052
|
+
return false
|
|
2053
|
+
}
|
|
2054
|
+
if (!isEditorReadyForNativeUpdate()) {
|
|
2055
|
+
return false
|
|
2056
|
+
}
|
|
2057
|
+
clearPendingEditorUpdateState(resetAppliedRevision = false)
|
|
2058
|
+
clearPendingViewCommandUpdateRetry()
|
|
2059
|
+
isApplyingJSUpdate = true
|
|
2060
|
+
val applied = try {
|
|
2061
|
+
richTextView.editorEditText.applyUpdateJSON(
|
|
2062
|
+
updateJson,
|
|
2063
|
+
refreshInputConnectionForExternalUpdate = true
|
|
2064
|
+
)
|
|
2065
|
+
clearPendingEditorUpdateDispatchQueue("jsResetUpdate")
|
|
2066
|
+
true
|
|
2067
|
+
} catch (error: Throwable) {
|
|
2068
|
+
Log.w(LOG_TAG, "Failed to apply JS editor reset update", error)
|
|
2069
|
+
false
|
|
2070
|
+
} finally {
|
|
2071
|
+
isApplyingJSUpdate = false
|
|
2072
|
+
}
|
|
2073
|
+
if (applied) {
|
|
2074
|
+
refreshReadyStateIfSettled()
|
|
2075
|
+
}
|
|
2076
|
+
return applied
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1703
2079
|
private fun isEditorReadyForNativeUpdate(): Boolean {
|
|
1704
2080
|
val editorId = richTextView.editorId
|
|
1705
2081
|
return editorId == 0L || (isAttachedToNativeWindow && richTextView.editorEditText.editorId == editorId)
|
|
@@ -1820,7 +2196,7 @@ class NativeEditorExpoView(
|
|
|
1820
2196
|
if (stateJson != null) {
|
|
1821
2197
|
event["stateJson"] = stateJson
|
|
1822
2198
|
}
|
|
1823
|
-
onSelectionChange(event)
|
|
2199
|
+
onSelectionChangeForTesting?.invoke(event) ?: onSelectionChange(event)
|
|
1824
2200
|
}
|
|
1825
2201
|
|
|
1826
2202
|
override fun onEditorUpdate(updateJSON: String) {
|
|
@@ -1925,7 +2301,7 @@ class NativeEditorExpoView(
|
|
|
1925
2301
|
"updateJson" to updateJSON,
|
|
1926
2302
|
"editorId" to event.editorId
|
|
1927
2303
|
)
|
|
1928
|
-
onEditorUpdate(payload)
|
|
2304
|
+
onEditorUpdateForTesting?.invoke(payload) ?: onEditorUpdate(payload)
|
|
1929
2305
|
}
|
|
1930
2306
|
val totalNanos = System.nanoTime() - startedAt
|
|
1931
2307
|
richTextView.editorEditText.recordImeTraceForTesting(
|
|
@@ -1936,23 +2312,81 @@ class NativeEditorExpoView(
|
|
|
1936
2312
|
|
|
1937
2313
|
private fun installOutsideTapBlurHandlerIfNeeded() {
|
|
1938
2314
|
val window = resolveActivity(context)?.window ?: return
|
|
1939
|
-
if (outsideTapWindow
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2315
|
+
if (outsideTapWindow !== window) {
|
|
2316
|
+
uninstallOutsideTapBlurHandler()
|
|
2317
|
+
}
|
|
2318
|
+
if (NativeEditorOutsideTapDispatcher.register(window, this)) {
|
|
2319
|
+
outsideTapWindow = window
|
|
2320
|
+
} else if (outsideTapWindow === window) {
|
|
2321
|
+
outsideTapWindow = null
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
private fun scheduleOutsideTapBlurHandlerInstallRetry() {
|
|
2326
|
+
cancelPendingOutsideTapBlurHandlerInstallRetry()
|
|
2327
|
+
val retry = Runnable {
|
|
2328
|
+
pendingOutsideTapHandlerInstallRetry = null
|
|
2329
|
+
if (richTextView.editorEditText.hasFocus()) {
|
|
2330
|
+
installOutsideTapBlurHandlerIfNeeded()
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
pendingOutsideTapHandlerInstallRetry = retry
|
|
2334
|
+
richTextView.editorEditText.postDelayed(retry, OUTSIDE_TAP_HANDLER_INSTALL_RETRY_DELAY_MS)
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
private fun cancelPendingOutsideTapBlurHandlerInstallRetry() {
|
|
2338
|
+
pendingOutsideTapHandlerInstallRetry?.let {
|
|
2339
|
+
richTextView.editorEditText.removeCallbacks(it)
|
|
2340
|
+
pendingOutsideTapHandlerInstallRetry = null
|
|
2341
|
+
}
|
|
1943
2342
|
}
|
|
1944
2343
|
|
|
1945
2344
|
private fun uninstallOutsideTapBlurHandler() {
|
|
2345
|
+
cancelPendingOutsideTapBlurHandlerInstallRetry()
|
|
1946
2346
|
val window = outsideTapWindow ?: return
|
|
1947
2347
|
NativeEditorOutsideTapDispatcher.unregister(window, this)
|
|
1948
2348
|
outsideTapWindow = null
|
|
1949
2349
|
}
|
|
1950
2350
|
|
|
1951
|
-
internal fun
|
|
1952
|
-
isAttachedToNativeWindow
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2351
|
+
internal fun prepareOutsideTapDecisionForWindowEvent(event: MotionEvent): NativeEditorOutsideTapDecision {
|
|
2352
|
+
if (!isAttachedToNativeWindow) {
|
|
2353
|
+
traceOutsideTap("decision ignored detached")
|
|
2354
|
+
return NativeEditorOutsideTapDecision.IGNORE
|
|
2355
|
+
}
|
|
2356
|
+
if (event.action != MotionEvent.ACTION_DOWN) {
|
|
2357
|
+
traceOutsideTap("decision ignored action=${event.action}")
|
|
2358
|
+
return NativeEditorOutsideTapDecision.IGNORE
|
|
2359
|
+
}
|
|
2360
|
+
if (!isEditorFocusedForOutsideTapDecision()) {
|
|
2361
|
+
traceOutsideTap("decision ignored not focused")
|
|
2362
|
+
return NativeEditorOutsideTapDecision.IGNORE
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
val decision = if (isTouchOutsideEditor(event)) {
|
|
2366
|
+
NativeEditorOutsideTapDecision.OUTSIDE_EDITOR
|
|
2367
|
+
} else {
|
|
2368
|
+
NativeEditorOutsideTapDecision.PRESERVE_FOCUS
|
|
2369
|
+
}
|
|
2370
|
+
traceOutsideTap("decision raw=${event.rawX.toInt()},${event.rawY.toInt()} value=$decision")
|
|
2371
|
+
return decision
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
internal fun handleOutsideTapDecisionFromWindowDispatcher(decision: NativeEditorOutsideTapDecision) {
|
|
2375
|
+
traceOutsideTap("handle decision=$decision")
|
|
2376
|
+
when (decision) {
|
|
2377
|
+
NativeEditorOutsideTapDecision.IGNORE -> {
|
|
2378
|
+
if (!richTextView.editorEditText.hasFocus()) {
|
|
2379
|
+
cancelPendingOutsideTapBlur()
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
NativeEditorOutsideTapDecision.PRESERVE_FOCUS -> cancelPendingOutsideTapBlur()
|
|
2383
|
+
NativeEditorOutsideTapDecision.OUTSIDE_EDITOR -> {
|
|
2384
|
+
clearRecentToolbarTouch()
|
|
2385
|
+
cancelPendingToolbarRefocus()
|
|
2386
|
+
scheduleOutsideTapBlur()
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
1956
2390
|
|
|
1957
2391
|
internal fun scheduleOutsideTapBlurFromWindowDispatcher() {
|
|
1958
2392
|
scheduleOutsideTapBlur()
|
|
@@ -1962,6 +2396,9 @@ class NativeEditorExpoView(
|
|
|
1962
2396
|
cancelPendingOutsideTapBlur()
|
|
1963
2397
|
}
|
|
1964
2398
|
|
|
2399
|
+
private fun isEditorFocusedForOutsideTapDecision(): Boolean =
|
|
2400
|
+
editorFocusedForOutsideTapOverrideForTesting ?: richTextView.editorEditText.hasFocus()
|
|
2401
|
+
|
|
1965
2402
|
private fun isTouchOutsideEditor(event: MotionEvent): Boolean {
|
|
1966
2403
|
if (isTouchInsideKeyboardToolbar(event)) {
|
|
1967
2404
|
markRecentToolbarTouch()
|
|
@@ -1973,7 +2410,11 @@ class NativeEditorExpoView(
|
|
|
1973
2410
|
}
|
|
1974
2411
|
val rect = Rect()
|
|
1975
2412
|
richTextView.editorEditText.getGlobalVisibleRect(rect)
|
|
1976
|
-
|
|
2413
|
+
val isOutside = !rect.contains(event.rawX.toInt(), event.rawY.toInt())
|
|
2414
|
+
if (isOutside) {
|
|
2415
|
+
clearRecentToolbarTouch()
|
|
2416
|
+
}
|
|
2417
|
+
return isOutside
|
|
1977
2418
|
}
|
|
1978
2419
|
|
|
1979
2420
|
private fun markRecentToolbarTouch() {
|
|
@@ -1990,6 +2431,14 @@ class NativeEditorExpoView(
|
|
|
1990
2431
|
return elapsedMs in 0L..TOOLBAR_FOCUS_PRESERVE_MS
|
|
1991
2432
|
}
|
|
1992
2433
|
|
|
2434
|
+
private fun consumeToolbarFocusPreservationForBlur(): Boolean {
|
|
2435
|
+
if (!shouldPreserveFocusAfterToolbarTouch()) {
|
|
2436
|
+
return false
|
|
2437
|
+
}
|
|
2438
|
+
clearRecentToolbarTouch()
|
|
2439
|
+
return true
|
|
2440
|
+
}
|
|
2441
|
+
|
|
1993
2442
|
internal fun markRecentToolbarTouchForTesting() {
|
|
1994
2443
|
markRecentToolbarTouch()
|
|
1995
2444
|
}
|
|
@@ -1997,6 +2446,10 @@ class NativeEditorExpoView(
|
|
|
1997
2446
|
internal fun shouldPreserveFocusAfterToolbarTouchForTesting(): Boolean =
|
|
1998
2447
|
shouldPreserveFocusAfterToolbarTouch()
|
|
1999
2448
|
|
|
2449
|
+
internal fun setEditorFocusedForOutsideTapDecisionForTesting(isFocused: Boolean?) {
|
|
2450
|
+
editorFocusedForOutsideTapOverrideForTesting = isFocused
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2000
2453
|
internal fun setAttachedToNativeWindowForTesting(isAttached: Boolean) {
|
|
2001
2454
|
isAttachedToNativeWindow = isAttached
|
|
2002
2455
|
}
|
|
@@ -2005,6 +2458,10 @@ class NativeEditorExpoView(
|
|
|
2005
2458
|
handleAttachedToWindow()
|
|
2006
2459
|
}
|
|
2007
2460
|
|
|
2461
|
+
internal fun traceOutsideTap(message: String) {
|
|
2462
|
+
onOutsideTapTraceForTesting?.invoke(message)
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2008
2465
|
internal fun handleDetachedFromWindowForTesting() {
|
|
2009
2466
|
prepareForDetachFromWindow()
|
|
2010
2467
|
handleDetachedFromWindow()
|
|
@@ -2021,6 +2478,8 @@ class NativeEditorExpoView(
|
|
|
2021
2478
|
|
|
2022
2479
|
internal fun hasPendingOutsideTapBlurForTesting(): Boolean = pendingOutsideTapBlur != null
|
|
2023
2480
|
|
|
2481
|
+
internal fun isOutsideTapBlurHandlerInstalledForTesting(): Boolean = outsideTapWindow != null
|
|
2482
|
+
|
|
2024
2483
|
internal fun hasPendingKeyboardDismissForTesting(): Boolean = pendingKeyboardDismiss != null
|
|
2025
2484
|
|
|
2026
2485
|
internal fun hasPendingPreflightWakeForTesting(): Boolean = pendingPreflightWakeScheduled
|
|
@@ -2043,6 +2502,10 @@ class NativeEditorExpoView(
|
|
|
2043
2502
|
scheduleToolbarRefocus()
|
|
2044
2503
|
}
|
|
2045
2504
|
|
|
2505
|
+
internal fun focusFromToolbarPreserveForTesting() {
|
|
2506
|
+
focusInternal(cancelPendingOutsideTapBlur = false)
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2046
2509
|
internal fun applyAutoFocusForTesting() {
|
|
2047
2510
|
applyAutoFocusIfNeeded()
|
|
2048
2511
|
}
|
|
@@ -2091,12 +2554,20 @@ class NativeEditorExpoView(
|
|
|
2091
2554
|
|
|
2092
2555
|
internal fun pendingEditorUpdateRevisionForTesting(): Int = pendingEditorUpdateRevision
|
|
2093
2556
|
|
|
2557
|
+
internal fun pendingEditorResetUpdateJsonForTesting(): String? = pendingEditorResetUpdateJson
|
|
2558
|
+
|
|
2559
|
+
internal fun pendingEditorResetUpdateRevisionForTesting(): Int =
|
|
2560
|
+
pendingEditorResetUpdateRevision
|
|
2561
|
+
|
|
2094
2562
|
internal fun setAppliedEditorUpdateRevisionForTesting(editorUpdateRevision: Int) {
|
|
2095
2563
|
appliedEditorUpdateRevision = editorUpdateRevision
|
|
2096
2564
|
}
|
|
2097
2565
|
|
|
2098
2566
|
internal fun pendingEditorUpdateEditorIdForTesting(): Long? = pendingEditorUpdateEditorId
|
|
2099
2567
|
|
|
2568
|
+
internal fun pendingEditorResetUpdateEditorIdForTesting(): Long? =
|
|
2569
|
+
pendingEditorResetUpdateEditorId
|
|
2570
|
+
|
|
2100
2571
|
internal fun pendingViewCommandUpdateJsonForTesting(): String? = pendingViewCommandUpdateJson
|
|
2101
2572
|
|
|
2102
2573
|
internal fun pendingViewCommandUpdateRetryAttemptsForTesting(): Int =
|
|
@@ -2136,10 +2607,10 @@ class NativeEditorExpoView(
|
|
|
2136
2607
|
if (toolbarFramesInWindow.isEmpty()) {
|
|
2137
2608
|
return false
|
|
2138
2609
|
}
|
|
2139
|
-
// toolbarFrame is in DP from React Native's measureInWindow
|
|
2140
|
-
//
|
|
2141
|
-
//
|
|
2142
|
-
//
|
|
2610
|
+
// toolbarFrame is in DP from React Native's measureInWindow, while
|
|
2611
|
+
// rawX/rawY are screen pixels. Normalize the event into the visible
|
|
2612
|
+
// window before comparing so shifted fallback rectangles cannot
|
|
2613
|
+
// preserve focus for unrelated outside taps.
|
|
2143
2614
|
val density = resources.displayMetrics.density
|
|
2144
2615
|
val hitSlopPx = TOOLBAR_HIT_SLOP_DP * density
|
|
2145
2616
|
val eventX = rawX - visibleWindowFrame.left
|
|
@@ -2153,14 +2624,7 @@ class NativeEditorExpoView(
|
|
|
2153
2624
|
).apply {
|
|
2154
2625
|
inset(-hitSlopPx, -hitSlopPx)
|
|
2155
2626
|
}
|
|
2156
|
-
|
|
2157
|
-
offset(visibleWindowFrame.left.toFloat(), visibleWindowFrame.top.toFloat())
|
|
2158
|
-
}
|
|
2159
|
-
if (
|
|
2160
|
-
windowFrameInPx.contains(rawX, rawY) ||
|
|
2161
|
-
windowFrameInPx.contains(eventX, eventY) ||
|
|
2162
|
-
screenFrameInPx.contains(rawX, rawY)
|
|
2163
|
-
) {
|
|
2627
|
+
if (windowFrameInPx.contains(eventX, eventY)) {
|
|
2164
2628
|
return true
|
|
2165
2629
|
}
|
|
2166
2630
|
}
|
|
@@ -2180,6 +2644,7 @@ class NativeEditorExpoView(
|
|
|
2180
2644
|
private const val TOOLBAR_HIT_SLOP_DP = 8f
|
|
2181
2645
|
private const val TOOLBAR_FOCUS_PRESERVE_MS = 750L
|
|
2182
2646
|
private const val OUTSIDE_TAP_BLUR_DELAY_MS = 100L
|
|
2647
|
+
private const val OUTSIDE_TAP_HANDLER_INSTALL_RETRY_DELAY_MS = 64L
|
|
2183
2648
|
private const val NATIVE_ACTION_RETRY_DELAY_MS = 16L
|
|
2184
2649
|
private const val EDITOR_UPDATE_EVENT_DEBOUNCE_MS = 64L
|
|
2185
2650
|
private const val PENDING_UPDATE_RECOVERY_RETRY_DELAY_MS = 250L
|
|
@@ -2191,6 +2656,7 @@ class NativeEditorExpoView(
|
|
|
2191
2656
|
}
|
|
2192
2657
|
|
|
2193
2658
|
private fun resolveActivity(context: Context): Activity? {
|
|
2659
|
+
appContext.currentActivity?.let { return it }
|
|
2194
2660
|
var current: Context? = context
|
|
2195
2661
|
while (current is ContextWrapper) {
|
|
2196
2662
|
if (current is Activity) return current
|