@apollohg/react-native-prose-editor 0.5.10 → 0.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +12 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +150 -23
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +4 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +2 -1
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +10 -0
- package/dist/EditorToolbar.d.ts +18 -1
- package/dist/EditorToolbar.js +156 -4
- package/dist/NativeRichTextEditor.d.ts +16 -0
- package/dist/NativeRichTextEditor.js +87 -11
- 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 +70 -8
- package/ios/NativeEditorModule.swift +3 -0
- package/ios/RichTextEditorView.swift +7 -0
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
|
@@ -551,6 +551,18 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
internal fun caretRect(): RectF? {
|
|
555
|
+
val textLayout = layout ?: return null
|
|
556
|
+
val selectionOffset = selectionEnd.takeIf { it >= 0 } ?: return null
|
|
557
|
+
val clampedOffset = selectionOffset.coerceIn(0, textLayout.text.length)
|
|
558
|
+
val line = textLayout.getLineForOffset(clampedOffset)
|
|
559
|
+
val caretLeft = textLayout.getPrimaryHorizontal(clampedOffset)
|
|
560
|
+
val left = totalPaddingLeft + caretLeft - scrollX
|
|
561
|
+
val top = totalPaddingTop + textLayout.getLineTop(line) - scrollY
|
|
562
|
+
val bottom = totalPaddingTop + textLayout.getLineBottom(line) - scrollY
|
|
563
|
+
return RectF(left, top.toFloat(), left + 1f, bottom.toFloat())
|
|
564
|
+
}
|
|
565
|
+
|
|
554
566
|
// ── Input Handling: Text Commit ─────────────────────────────────────
|
|
555
567
|
|
|
556
568
|
/**
|
|
@@ -5,6 +5,7 @@ import android.content.Context
|
|
|
5
5
|
import android.content.ContextWrapper
|
|
6
6
|
import android.graphics.Rect
|
|
7
7
|
import android.graphics.RectF
|
|
8
|
+
import android.os.SystemClock
|
|
8
9
|
import android.view.Gravity
|
|
9
10
|
import android.view.MotionEvent
|
|
10
11
|
import android.view.View
|
|
@@ -61,7 +62,8 @@ class NativeEditorExpoView(
|
|
|
61
62
|
private var lastEmittedContentHeight = 0
|
|
62
63
|
private var outsideTapWindowCallback: Window.Callback? = null
|
|
63
64
|
private var previousWindowCallback: Window.Callback? = null
|
|
64
|
-
private var
|
|
65
|
+
private var toolbarFramesInWindow: List<RectF> = emptyList()
|
|
66
|
+
private var lastToolbarTouchUptimeMs: Long? = null
|
|
65
67
|
private var addons = NativeEditorAddons(null)
|
|
66
68
|
private var mentionQueryState: MentionQueryState? = null
|
|
67
69
|
private var lastMentionEventJson: String? = null
|
|
@@ -91,7 +93,7 @@ class NativeEditorExpoView(
|
|
|
91
93
|
ViewCompat.setOnApplyWindowInsetsListener(keyboardToolbarView) { _, insets ->
|
|
92
94
|
currentImeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
93
95
|
updateKeyboardToolbarLayout()
|
|
94
|
-
|
|
96
|
+
updateAttachedKeyboardToolbarForInsets()
|
|
95
97
|
insets
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -101,6 +103,12 @@ class NativeEditorExpoView(
|
|
|
101
103
|
installOutsideTapBlurHandlerIfNeeded()
|
|
102
104
|
refreshMentionQuery()
|
|
103
105
|
} else {
|
|
106
|
+
if (shouldPreserveFocusAfterToolbarTouch()) {
|
|
107
|
+
richTextView.editorEditText.post {
|
|
108
|
+
focus()
|
|
109
|
+
}
|
|
110
|
+
return@setOnFocusChangeListener
|
|
111
|
+
}
|
|
104
112
|
uninstallOutsideTapBlurHandler()
|
|
105
113
|
clearMentionQueryState()
|
|
106
114
|
}
|
|
@@ -195,23 +203,52 @@ class NativeEditorExpoView(
|
|
|
195
203
|
if (lastToolbarFrameJson == toolbarFrameJson) return
|
|
196
204
|
lastToolbarFrameJson = toolbarFrameJson
|
|
197
205
|
if (toolbarFrameJson.isNullOrBlank()) {
|
|
198
|
-
|
|
206
|
+
toolbarFramesInWindow = emptyList()
|
|
199
207
|
return
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
|
|
210
|
+
toolbarFramesInWindow = try {
|
|
203
211
|
val json = JSONObject(toolbarFrameJson)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
val frames = json.optJSONArray("frames")
|
|
213
|
+
if (frames != null) {
|
|
214
|
+
buildList {
|
|
215
|
+
for (index in 0 until frames.length()) {
|
|
216
|
+
frames.optJSONObject(index)?.toToolbarFrame()?.let { add(it) }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
listOfNotNull(json.toToolbarFrame())
|
|
221
|
+
}
|
|
210
222
|
} catch (_: Throwable) {
|
|
211
|
-
|
|
223
|
+
emptyList()
|
|
212
224
|
}
|
|
213
225
|
}
|
|
214
226
|
|
|
227
|
+
private fun JSONObject.toToolbarFrame(): RectF? {
|
|
228
|
+
val x = optDouble("x", Double.NaN)
|
|
229
|
+
val y = optDouble("y", Double.NaN)
|
|
230
|
+
val width = optDouble("width", Double.NaN)
|
|
231
|
+
val height = optDouble("height", Double.NaN)
|
|
232
|
+
if (
|
|
233
|
+
x.isNaN() || x.isInfinite() ||
|
|
234
|
+
y.isNaN() || y.isInfinite() ||
|
|
235
|
+
width.isNaN() || width.isInfinite() ||
|
|
236
|
+
height.isNaN() || height.isInfinite()
|
|
237
|
+
) {
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
240
|
+
if (width <= 0.0 || height <= 0.0) {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return RectF(
|
|
245
|
+
x.toFloat(),
|
|
246
|
+
y.toFloat(),
|
|
247
|
+
(x + width).toFloat(),
|
|
248
|
+
(y + height).toFloat()
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
215
252
|
fun setPendingEditorUpdateJson(editorUpdateJson: String?) {
|
|
216
253
|
pendingEditorUpdateJson = editorUpdateJson
|
|
217
254
|
}
|
|
@@ -230,14 +267,33 @@ class NativeEditorExpoView(
|
|
|
230
267
|
|
|
231
268
|
fun focus() {
|
|
232
269
|
richTextView.editorEditText.requestFocus()
|
|
270
|
+
richTextView.editorEditText.post {
|
|
271
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
272
|
+
imm?.showSoftInput(richTextView.editorEditText, InputMethodManager.SHOW_IMPLICIT)
|
|
273
|
+
}
|
|
233
274
|
}
|
|
234
275
|
|
|
235
276
|
fun blur() {
|
|
277
|
+
clearRecentToolbarTouch()
|
|
236
278
|
richTextView.editorEditText.clearFocus()
|
|
237
279
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
238
280
|
imm?.hideSoftInputFromWindow(richTextView.editorEditText.windowToken, 0)
|
|
239
281
|
}
|
|
240
282
|
|
|
283
|
+
fun getCaretRectJson(): String? {
|
|
284
|
+
if (width <= 0 || height <= 0) return null
|
|
285
|
+
val rect = richTextView.caretRect() ?: return null
|
|
286
|
+
val density = resources.displayMetrics.density
|
|
287
|
+
return JSONObject()
|
|
288
|
+
.put("x", rect.left / density)
|
|
289
|
+
.put("y", rect.top / density)
|
|
290
|
+
.put("width", rect.width() / density)
|
|
291
|
+
.put("height", rect.height() / density)
|
|
292
|
+
.put("editorWidth", width / density)
|
|
293
|
+
.put("editorHeight", height / density)
|
|
294
|
+
.toString()
|
|
295
|
+
}
|
|
296
|
+
|
|
241
297
|
override fun onDetachedFromWindow() {
|
|
242
298
|
super.onDetachedFromWindow()
|
|
243
299
|
uninstallOutsideTapBlurHandler()
|
|
@@ -404,24 +460,85 @@ class NativeEditorExpoView(
|
|
|
404
460
|
if (isTouchInsideKeyboardToolbar(event)) {
|
|
405
461
|
return false
|
|
406
462
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
463
|
+
if (isTouchInsideStandaloneToolbar(event)) {
|
|
464
|
+
markRecentToolbarTouch()
|
|
465
|
+
return false
|
|
466
|
+
}
|
|
467
|
+
val rect = Rect()
|
|
468
|
+
richTextView.editorEditText.getGlobalVisibleRect(rect)
|
|
469
|
+
return !rect.contains(event.rawX.toInt(), event.rawY.toInt())
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private fun markRecentToolbarTouch() {
|
|
473
|
+
lastToolbarTouchUptimeMs = SystemClock.uptimeMillis()
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private fun clearRecentToolbarTouch() {
|
|
477
|
+
lastToolbarTouchUptimeMs = null
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private fun shouldPreserveFocusAfterToolbarTouch(): Boolean {
|
|
481
|
+
val lastToolbarTouch = lastToolbarTouchUptimeMs ?: return false
|
|
482
|
+
val elapsedMs = SystemClock.uptimeMillis() - lastToolbarTouch
|
|
483
|
+
return elapsedMs in 0L..TOOLBAR_FOCUS_PRESERVE_MS
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
internal fun markRecentToolbarTouchForTesting() {
|
|
487
|
+
markRecentToolbarTouch()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
internal fun shouldPreserveFocusAfterToolbarTouchForTesting(): Boolean =
|
|
491
|
+
shouldPreserveFocusAfterToolbarTouch()
|
|
492
|
+
|
|
493
|
+
private fun isTouchInsideStandaloneToolbar(event: MotionEvent): Boolean {
|
|
494
|
+
val visibleWindowFrame = Rect()
|
|
495
|
+
getWindowVisibleDisplayFrame(visibleWindowFrame)
|
|
496
|
+
return isPointInsideStandaloneToolbar(event.rawX, event.rawY, visibleWindowFrame)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
internal fun isPointInsideStandaloneToolbarForTesting(
|
|
500
|
+
rawX: Float,
|
|
501
|
+
rawY: Float,
|
|
502
|
+
visibleWindowFrame: Rect
|
|
503
|
+
): Boolean = isPointInsideStandaloneToolbar(rawX, rawY, visibleWindowFrame)
|
|
504
|
+
|
|
505
|
+
private fun isPointInsideStandaloneToolbar(
|
|
506
|
+
rawX: Float,
|
|
507
|
+
rawY: Float,
|
|
508
|
+
visibleWindowFrame: Rect
|
|
509
|
+
): Boolean {
|
|
510
|
+
if (toolbarFramesInWindow.isEmpty()) {
|
|
511
|
+
return false
|
|
512
|
+
}
|
|
513
|
+
// toolbarFrame is in DP from React Native's measureInWindow. On Android
|
|
514
|
+
// that is window-relative after visible-window insets are subtracted,
|
|
515
|
+
// while rawX/rawY are screen pixels. Fabric/newer implementations may
|
|
516
|
+
// differ here, so accept both window-relative and raw-screen comparisons.
|
|
517
|
+
val density = resources.displayMetrics.density
|
|
518
|
+
val hitSlopPx = TOOLBAR_HIT_SLOP_DP * density
|
|
519
|
+
val eventX = rawX - visibleWindowFrame.left
|
|
520
|
+
val eventY = rawY - visibleWindowFrame.top
|
|
521
|
+
for (toolbarFrame in toolbarFramesInWindow) {
|
|
522
|
+
val windowFrameInPx = RectF(
|
|
413
523
|
toolbarFrame.left * density,
|
|
414
524
|
toolbarFrame.top * density,
|
|
415
525
|
toolbarFrame.right * density,
|
|
416
526
|
toolbarFrame.bottom * density
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
|
|
527
|
+
).apply {
|
|
528
|
+
inset(-hitSlopPx, -hitSlopPx)
|
|
529
|
+
}
|
|
530
|
+
val screenFrameInPx = RectF(windowFrameInPx).apply {
|
|
531
|
+
offset(visibleWindowFrame.left.toFloat(), visibleWindowFrame.top.toFloat())
|
|
532
|
+
}
|
|
533
|
+
if (
|
|
534
|
+
windowFrameInPx.contains(rawX, rawY) ||
|
|
535
|
+
windowFrameInPx.contains(eventX, eventY) ||
|
|
536
|
+
screenFrameInPx.contains(rawX, rawY)
|
|
537
|
+
) {
|
|
538
|
+
return true
|
|
420
539
|
}
|
|
421
540
|
}
|
|
422
|
-
|
|
423
|
-
richTextView.editorEditText.getGlobalVisibleRect(rect)
|
|
424
|
-
return !rect.contains(event.rawX.toInt(), event.rawY.toInt())
|
|
541
|
+
return false
|
|
425
542
|
}
|
|
426
543
|
|
|
427
544
|
private fun isTouchInsideKeyboardToolbar(event: MotionEvent): Boolean {
|
|
@@ -433,6 +550,11 @@ class NativeEditorExpoView(
|
|
|
433
550
|
return rect.contains(event.rawX.toInt(), event.rawY.toInt())
|
|
434
551
|
}
|
|
435
552
|
|
|
553
|
+
private companion object {
|
|
554
|
+
private const val TOOLBAR_HIT_SLOP_DP = 8f
|
|
555
|
+
private const val TOOLBAR_FOCUS_PRESERVE_MS = 750L
|
|
556
|
+
}
|
|
557
|
+
|
|
436
558
|
private fun resolveActivity(context: Context): Activity? {
|
|
437
559
|
var current: Context? = context
|
|
438
560
|
while (current is ContextWrapper) {
|
|
@@ -658,6 +780,11 @@ class NativeEditorExpoView(
|
|
|
658
780
|
keyboardToolbarView.layoutParams = params
|
|
659
781
|
}
|
|
660
782
|
|
|
783
|
+
private fun updateAttachedKeyboardToolbarForInsets() {
|
|
784
|
+
keyboardToolbarView.visibility = if (currentImeBottom > 0) View.VISIBLE else View.INVISIBLE
|
|
785
|
+
updateEditorViewportInset()
|
|
786
|
+
}
|
|
787
|
+
|
|
661
788
|
private fun updateKeyboardToolbarVisibility() {
|
|
662
789
|
val shouldAttach =
|
|
663
790
|
showsToolbar &&
|
|
@@ -380,6 +380,10 @@ class NativeEditorModule : Module() {
|
|
|
380
380
|
view.blur()
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
AsyncFunction("getCaretRect") { view: NativeEditorExpoView ->
|
|
384
|
+
view.getCaretRectJson()
|
|
385
|
+
}
|
|
386
|
+
|
|
383
387
|
AsyncFunction("applyEditorUpdate") { view: NativeEditorExpoView, updateJson: String ->
|
|
384
388
|
view.applyEditorUpdate(updateJson)
|
|
385
389
|
}
|
|
@@ -11,6 +11,7 @@ import android.view.View
|
|
|
11
11
|
import android.view.ViewOutlineProvider
|
|
12
12
|
import android.widget.HorizontalScrollView
|
|
13
13
|
import android.widget.LinearLayout
|
|
14
|
+
import androidx.appcompat.R as AppCompatR
|
|
14
15
|
import androidx.appcompat.widget.AppCompatButton
|
|
15
16
|
import androidx.appcompat.widget.PopupMenu
|
|
16
17
|
import androidx.appcompat.widget.AppCompatTextView
|
|
@@ -834,7 +835,7 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
834
835
|
0.38f
|
|
835
836
|
)
|
|
836
837
|
active -> theme?.buttonActiveColor ?: resolveColorAttr(
|
|
837
|
-
|
|
838
|
+
AppCompatR.attr.colorPrimary,
|
|
838
839
|
android.R.attr.textColorPrimary
|
|
839
840
|
)
|
|
840
841
|
else -> theme?.buttonColor ?: resolveColorAttr(
|
|
@@ -301,6 +301,16 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
301
301
|
)
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
internal fun caretRect(): RectF? {
|
|
305
|
+
val rect = editorEditText.caretRect() ?: return null
|
|
306
|
+
return RectF(
|
|
307
|
+
editorViewport.left + editorScrollView.left + editorEditText.left + rect.left,
|
|
308
|
+
editorViewport.top + editorScrollView.top + editorEditText.top + rect.top - editorScrollView.scrollY,
|
|
309
|
+
editorViewport.left + editorScrollView.left + editorEditText.left + rect.right,
|
|
310
|
+
editorViewport.top + editorScrollView.top + editorEditText.top + rect.bottom - editorScrollView.scrollY
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
304
314
|
internal fun maximumImageWidthPx(): Float {
|
|
305
315
|
val availableWidth =
|
|
306
316
|
maxOf(editorEditText.width, editorEditText.measuredWidth) -
|
package/dist/EditorToolbar.d.ts
CHANGED
|
@@ -91,6 +91,18 @@ export type EditorToolbarItem = EditorToolbarLeafItem | EditorToolbarGroupItem |
|
|
|
91
91
|
type: 'separator';
|
|
92
92
|
key?: string;
|
|
93
93
|
};
|
|
94
|
+
export interface EditorToolbarFrame {
|
|
95
|
+
x: number;
|
|
96
|
+
y: number;
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
}
|
|
100
|
+
export declare function isEditorToolbarFocusPreservationActive(): boolean;
|
|
101
|
+
export declare function useEditorToolbarFrames(): readonly EditorToolbarFrame[];
|
|
102
|
+
export declare function _setEditorToolbarFrameForTests(id: number, frame: EditorToolbarFrame | null): void;
|
|
103
|
+
export declare function _resetEditorToolbarFrameRegistryForTests(): void;
|
|
104
|
+
export declare function _beginEditorToolbarInteractionForTests(): void;
|
|
105
|
+
export declare function _endEditorToolbarInteractionForTests(): void;
|
|
94
106
|
export declare const DEFAULT_EDITOR_TOOLBAR_ITEMS: readonly EditorToolbarItem[];
|
|
95
107
|
export interface EditorToolbarProps {
|
|
96
108
|
/** Currently active marks and nodes from the Rust engine. */
|
|
@@ -145,5 +157,10 @@ export interface EditorToolbarProps {
|
|
|
145
157
|
theme?: EditorToolbarTheme;
|
|
146
158
|
/** Whether to render the built-in top separator line. */
|
|
147
159
|
showTopBorder?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Keep NativeRichTextEditor focused when this toolbar is rendered outside
|
|
162
|
+
* the editor wrapper. Defaults to true.
|
|
163
|
+
*/
|
|
164
|
+
preserveEditorFocus?: boolean;
|
|
148
165
|
}
|
|
149
|
-
export declare function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems, theme, showTopBorder, }: EditorToolbarProps): import("react/jsx-runtime").JSX.Element;
|
|
166
|
+
export declare function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems, theme, showTopBorder, preserveEditorFocus, }: EditorToolbarProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/EditorToolbar.js
CHANGED
|
@@ -1,11 +1,95 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = void 0;
|
|
4
|
+
exports.isEditorToolbarFocusPreservationActive = isEditorToolbarFocusPreservationActive;
|
|
5
|
+
exports.useEditorToolbarFrames = useEditorToolbarFrames;
|
|
6
|
+
exports._setEditorToolbarFrameForTests = _setEditorToolbarFrameForTests;
|
|
7
|
+
exports._resetEditorToolbarFrameRegistryForTests = _resetEditorToolbarFrameRegistryForTests;
|
|
8
|
+
exports._beginEditorToolbarInteractionForTests = _beginEditorToolbarInteractionForTests;
|
|
9
|
+
exports._endEditorToolbarInteractionForTests = _endEditorToolbarInteractionForTests;
|
|
4
10
|
exports.EditorToolbar = EditorToolbar;
|
|
5
11
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
12
|
const vector_icons_1 = require("@expo/vector-icons");
|
|
7
13
|
const react_1 = require("react");
|
|
8
14
|
const react_native_1 = require("react-native");
|
|
15
|
+
const editorToolbarFrames = new Map();
|
|
16
|
+
const editorToolbarFrameListeners = new Set();
|
|
17
|
+
let nextEditorToolbarRegistrationId = 1;
|
|
18
|
+
let activeEditorToolbarInteractions = 0;
|
|
19
|
+
let editorToolbarFocusPreserveUntil = 0;
|
|
20
|
+
const EDITOR_TOOLBAR_FOCUS_PRESERVE_MS = 750;
|
|
21
|
+
function areToolbarFramesEqual(left, right) {
|
|
22
|
+
return (left?.x === right?.x &&
|
|
23
|
+
left?.y === right?.y &&
|
|
24
|
+
left?.width === right?.width &&
|
|
25
|
+
left?.height === right?.height);
|
|
26
|
+
}
|
|
27
|
+
function notifyEditorToolbarFrameListeners() {
|
|
28
|
+
editorToolbarFrameListeners.forEach((listener) => listener());
|
|
29
|
+
}
|
|
30
|
+
function getEditorToolbarFramesSnapshot() {
|
|
31
|
+
return Array.from(editorToolbarFrames.values());
|
|
32
|
+
}
|
|
33
|
+
function registerEditorToolbarFrame(id, frame) {
|
|
34
|
+
if (frame == null || frame.width <= 0 || frame.height <= 0) {
|
|
35
|
+
if (editorToolbarFrames.delete(id)) {
|
|
36
|
+
notifyEditorToolbarFrameListeners();
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const currentFrame = editorToolbarFrames.get(id);
|
|
41
|
+
if (areToolbarFramesEqual(currentFrame, frame)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
editorToolbarFrames.set(id, frame);
|
|
45
|
+
notifyEditorToolbarFrameListeners();
|
|
46
|
+
}
|
|
47
|
+
function unregisterEditorToolbarFrame(id) {
|
|
48
|
+
if (editorToolbarFrames.delete(id)) {
|
|
49
|
+
notifyEditorToolbarFrameListeners();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function preserveEditorToolbarFocusForNextBlur() {
|
|
53
|
+
editorToolbarFocusPreserveUntil = Date.now() + EDITOR_TOOLBAR_FOCUS_PRESERVE_MS;
|
|
54
|
+
}
|
|
55
|
+
function beginEditorToolbarInteraction() {
|
|
56
|
+
activeEditorToolbarInteractions += 1;
|
|
57
|
+
preserveEditorToolbarFocusForNextBlur();
|
|
58
|
+
}
|
|
59
|
+
function endEditorToolbarInteraction() {
|
|
60
|
+
activeEditorToolbarInteractions = Math.max(0, activeEditorToolbarInteractions - 1);
|
|
61
|
+
preserveEditorToolbarFocusForNextBlur();
|
|
62
|
+
}
|
|
63
|
+
function isEditorToolbarFocusPreservationActive() {
|
|
64
|
+
return activeEditorToolbarInteractions > 0 || Date.now() <= editorToolbarFocusPreserveUntil;
|
|
65
|
+
}
|
|
66
|
+
function useEditorToolbarFrames() {
|
|
67
|
+
const [frames, setFrames] = (0, react_1.useState)(getEditorToolbarFramesSnapshot);
|
|
68
|
+
(0, react_1.useEffect)(() => {
|
|
69
|
+
const listener = () => setFrames(getEditorToolbarFramesSnapshot());
|
|
70
|
+
editorToolbarFrameListeners.add(listener);
|
|
71
|
+
listener();
|
|
72
|
+
return () => {
|
|
73
|
+
editorToolbarFrameListeners.delete(listener);
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
return frames;
|
|
77
|
+
}
|
|
78
|
+
function _setEditorToolbarFrameForTests(id, frame) {
|
|
79
|
+
registerEditorToolbarFrame(id, frame);
|
|
80
|
+
}
|
|
81
|
+
function _resetEditorToolbarFrameRegistryForTests() {
|
|
82
|
+
editorToolbarFrames.clear();
|
|
83
|
+
activeEditorToolbarInteractions = 0;
|
|
84
|
+
editorToolbarFocusPreserveUntil = 0;
|
|
85
|
+
notifyEditorToolbarFrameListeners();
|
|
86
|
+
}
|
|
87
|
+
function _beginEditorToolbarInteractionForTests() {
|
|
88
|
+
beginEditorToolbarInteraction();
|
|
89
|
+
}
|
|
90
|
+
function _endEditorToolbarInteractionForTests() {
|
|
91
|
+
endEditorToolbarInteraction();
|
|
92
|
+
}
|
|
9
93
|
function defaultIcon(id) {
|
|
10
94
|
return { type: 'default', id };
|
|
11
95
|
}
|
|
@@ -109,16 +193,22 @@ const DEFAULT_MATERIAL_ICONS = {
|
|
|
109
193
|
undo: 'undo',
|
|
110
194
|
redo: 'redo',
|
|
111
195
|
};
|
|
112
|
-
function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder, }) {
|
|
196
|
+
function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder, preserveEditorFocus = true, }) {
|
|
113
197
|
const marks = activeState.marks ?? {};
|
|
114
198
|
const nodes = activeState.nodes ?? {};
|
|
115
199
|
const commands = activeState.commands ?? {};
|
|
116
200
|
const allowedMarks = activeState.allowedMarks ?? [];
|
|
117
201
|
const insertableNodes = activeState.insertableNodes ?? [];
|
|
202
|
+
const rootRef = (0, react_1.useRef)(null);
|
|
118
203
|
const groupButtonRefs = (0, react_1.useRef)(new Map());
|
|
119
204
|
const { width: windowWidth, height: windowHeight } = (0, react_native_1.useWindowDimensions)();
|
|
120
205
|
const [expandedGroupKey, setExpandedGroupKey] = (0, react_1.useState)(null);
|
|
121
206
|
const [menuState, setMenuState] = (0, react_1.useState)(null);
|
|
207
|
+
const toolbarInteractionActiveRef = (0, react_1.useRef)(false);
|
|
208
|
+
const registrationIdRef = (0, react_1.useRef)(null);
|
|
209
|
+
if (registrationIdRef.current == null) {
|
|
210
|
+
registrationIdRef.current = nextEditorToolbarRegistrationId++;
|
|
211
|
+
}
|
|
122
212
|
const isMarkActive = (0, react_1.useCallback)((mark) => !!marks[mark], [marks]);
|
|
123
213
|
const isInList = !!nodes['bulletList'] || !!nodes['orderedList'];
|
|
124
214
|
const canIndentList = isInList && !!commands['indentList'];
|
|
@@ -372,6 +462,56 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
372
462
|
};
|
|
373
463
|
}, [expandedGroupKey, menuState?.groupKey, resolveButton, toolbarItems]);
|
|
374
464
|
const resolvedShowTopBorder = showTopBorder ?? theme?.showTopBorder ?? true;
|
|
465
|
+
const publishToolbarFrame = (0, react_1.useCallback)(() => {
|
|
466
|
+
const registrationId = registrationIdRef.current;
|
|
467
|
+
const toolbar = rootRef.current;
|
|
468
|
+
if (!preserveEditorFocus || registrationId == null || !toolbar) {
|
|
469
|
+
if (registrationId != null) {
|
|
470
|
+
unregisterEditorToolbarFrame(registrationId);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (typeof toolbar.measureInWindow !== 'function') {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
toolbar.measureInWindow((x, y, width, height) => {
|
|
478
|
+
registerEditorToolbarFrame(registrationId, { x, y, width, height });
|
|
479
|
+
});
|
|
480
|
+
}, [preserveEditorFocus]);
|
|
481
|
+
const handleToolbarLayout = (0, react_1.useCallback)(() => {
|
|
482
|
+
requestAnimationFrame(publishToolbarFrame);
|
|
483
|
+
}, [publishToolbarFrame]);
|
|
484
|
+
(0, react_1.useEffect)(() => {
|
|
485
|
+
if (!preserveEditorFocus) {
|
|
486
|
+
const registrationId = registrationIdRef.current;
|
|
487
|
+
if (registrationId != null) {
|
|
488
|
+
unregisterEditorToolbarFrame(registrationId);
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const frame = requestAnimationFrame(publishToolbarFrame);
|
|
493
|
+
return () => cancelAnimationFrame(frame);
|
|
494
|
+
}, [
|
|
495
|
+
expandedGroupKey,
|
|
496
|
+
menuState?.groupKey,
|
|
497
|
+
preserveEditorFocus,
|
|
498
|
+
publishToolbarFrame,
|
|
499
|
+
renderedItems.length,
|
|
500
|
+
windowHeight,
|
|
501
|
+
windowWidth,
|
|
502
|
+
]);
|
|
503
|
+
(0, react_1.useEffect)(() => {
|
|
504
|
+
const registrationId = registrationIdRef.current;
|
|
505
|
+
return () => {
|
|
506
|
+
if (toolbarInteractionActiveRef.current) {
|
|
507
|
+
toolbarInteractionActiveRef.current = false;
|
|
508
|
+
endEditorToolbarInteraction();
|
|
509
|
+
}
|
|
510
|
+
if (registrationId != null) {
|
|
511
|
+
unregisterEditorToolbarFrame(registrationId);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}, []);
|
|
375
515
|
(0, react_1.useEffect)(() => {
|
|
376
516
|
if (expandedGroupKey != null && !groupsByKey.has(expandedGroupKey)) {
|
|
377
517
|
setExpandedGroupKey(null);
|
|
@@ -389,6 +529,18 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
389
529
|
}
|
|
390
530
|
setMenuState(null);
|
|
391
531
|
}, []);
|
|
532
|
+
const handleToolbarPressIn = (0, react_1.useCallback)(() => {
|
|
533
|
+
if (preserveEditorFocus && !toolbarInteractionActiveRef.current) {
|
|
534
|
+
toolbarInteractionActiveRef.current = true;
|
|
535
|
+
beginEditorToolbarInteraction();
|
|
536
|
+
}
|
|
537
|
+
}, [preserveEditorFocus]);
|
|
538
|
+
const handleToolbarPressOut = (0, react_1.useCallback)(() => {
|
|
539
|
+
if (preserveEditorFocus && toolbarInteractionActiveRef.current) {
|
|
540
|
+
toolbarInteractionActiveRef.current = false;
|
|
541
|
+
endEditorToolbarInteraction();
|
|
542
|
+
}
|
|
543
|
+
}, [preserveEditorFocus]);
|
|
392
544
|
const handleGroupPress = (0, react_1.useCallback)((group) => {
|
|
393
545
|
if (group.isDisabled) {
|
|
394
546
|
return;
|
|
@@ -442,7 +594,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
442
594
|
else {
|
|
443
595
|
groupButtonRefs.current.delete(anchorGroupKey);
|
|
444
596
|
}
|
|
445
|
-
}, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: onPress, disabled: button.isDisabled, style: [
|
|
597
|
+
}, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: onPress, disabled: button.isDisabled, style: [
|
|
446
598
|
styles.button,
|
|
447
599
|
{
|
|
448
600
|
borderRadius: theme?.buttonBorderRadius ?? BUTTON_RADIUS,
|
|
@@ -460,7 +612,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
460
612
|
styles.separator,
|
|
461
613
|
theme?.separatorColor != null ? { backgroundColor: theme.separatorColor } : null,
|
|
462
614
|
] }, key));
|
|
463
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
|
|
615
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: rootRef, collapsable: false, onLayout: handleToolbarLayout, style: [
|
|
464
616
|
styles.container,
|
|
465
617
|
!resolvedShowTopBorder && styles.containerWithoutTopBorder,
|
|
466
618
|
theme?.backgroundColor != null ? { backgroundColor: theme.backgroundColor } : null,
|
|
@@ -512,7 +664,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
512
664
|
: button.isDisabled
|
|
513
665
|
? disabledColor
|
|
514
666
|
: defaultColor;
|
|
515
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
|
|
667
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
|
|
516
668
|
styles.menuItem,
|
|
517
669
|
button.isActive && {
|
|
518
670
|
backgroundColor: theme?.buttonActiveBackgroundColor ?? ACTIVE_BG,
|
|
@@ -135,6 +135,8 @@ export interface NativeRichTextEditorRef {
|
|
|
135
135
|
getContentJson(): DocumentJSON;
|
|
136
136
|
/** Get the plain text content (no markup). */
|
|
137
137
|
getTextContent(): string;
|
|
138
|
+
/** Get the current caret rectangle in editor-local layout coordinates. */
|
|
139
|
+
getCaretRect(): Promise<NativeRichTextEditorCaretRect | null>;
|
|
138
140
|
/** Undo the last operation. */
|
|
139
141
|
undo(): void;
|
|
140
142
|
/** Redo the last undone operation. */
|
|
@@ -144,4 +146,18 @@ export interface NativeRichTextEditorRef {
|
|
|
144
146
|
/** Check if redo is available. */
|
|
145
147
|
canRedo(): boolean;
|
|
146
148
|
}
|
|
149
|
+
export interface NativeRichTextEditorCaretRect {
|
|
150
|
+
/** Left edge of the caret, relative to the editor root view. */
|
|
151
|
+
x: number;
|
|
152
|
+
/** Top edge of the caret, relative to the editor root view. */
|
|
153
|
+
y: number;
|
|
154
|
+
/** Caret width. */
|
|
155
|
+
width: number;
|
|
156
|
+
/** Caret height. */
|
|
157
|
+
height: number;
|
|
158
|
+
/** Current editor root view width. */
|
|
159
|
+
editorWidth: number;
|
|
160
|
+
/** Current editor root view height. */
|
|
161
|
+
editorHeight: number;
|
|
162
|
+
}
|
|
147
163
|
export declare const NativeRichTextEditor: React.ForwardRefExoticComponent<NativeRichTextEditorProps & React.RefAttributes<NativeRichTextEditorRef>>;
|
|
@@ -423,6 +423,44 @@ function serializeRemoteSelections(remoteSelections) {
|
|
|
423
423
|
}
|
|
424
424
|
return stringifyCachedJson(remoteSelections);
|
|
425
425
|
}
|
|
426
|
+
function areToolbarFramesEqual(left, right) {
|
|
427
|
+
return (left?.x === right?.x &&
|
|
428
|
+
left?.y === right?.y &&
|
|
429
|
+
left?.width === right?.width &&
|
|
430
|
+
left?.height === right?.height);
|
|
431
|
+
}
|
|
432
|
+
function serializeToolbarFrames(frames) {
|
|
433
|
+
if (!frames || frames.length === 0) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
return JSON.stringify(frames.length === 1 ? frames[0] : { frames });
|
|
437
|
+
}
|
|
438
|
+
function parseCaretRectJson(raw) {
|
|
439
|
+
if (!raw) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const parsed = JSON.parse(raw);
|
|
444
|
+
const x = typeof parsed.x === 'number' ? parsed.x : null;
|
|
445
|
+
const y = typeof parsed.y === 'number' ? parsed.y : null;
|
|
446
|
+
const width = typeof parsed.width === 'number' ? parsed.width : null;
|
|
447
|
+
const height = typeof parsed.height === 'number' ? parsed.height : null;
|
|
448
|
+
const editorWidth = typeof parsed.editorWidth === 'number' ? parsed.editorWidth : null;
|
|
449
|
+
const editorHeight = typeof parsed.editorHeight === 'number' ? parsed.editorHeight : null;
|
|
450
|
+
if (x == null ||
|
|
451
|
+
y == null ||
|
|
452
|
+
width == null ||
|
|
453
|
+
height == null ||
|
|
454
|
+
editorWidth == null ||
|
|
455
|
+
editorHeight == null) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return { x, y, width, height, editorWidth, editorHeight };
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
426
464
|
const serializedJsonCache = new WeakMap();
|
|
427
465
|
function stringifyCachedJson(value) {
|
|
428
466
|
if (value != null && typeof value === 'object') {
|
|
@@ -463,7 +501,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
463
501
|
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
464
502
|
const [editorInstanceId, setEditorInstanceId] = (0, react_1.useState)(0);
|
|
465
503
|
const [isFocused, setIsFocused] = (0, react_1.useState)(false);
|
|
466
|
-
const
|
|
504
|
+
const isFocusedRef = (0, react_1.useRef)(false);
|
|
505
|
+
const [inlineToolbarFrame, setInlineToolbarFrame] = (0, react_1.useState)(null);
|
|
506
|
+
const registeredToolbarFrames = (0, EditorToolbar_1.useEditorToolbarFrames)();
|
|
467
507
|
const [pendingNativeUpdate, setPendingNativeUpdate] = (0, react_1.useState)({
|
|
468
508
|
json: undefined,
|
|
469
509
|
revision: 0,
|
|
@@ -745,21 +785,21 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
745
785
|
const updateToolbarFrame = (0, react_1.useCallback)(() => {
|
|
746
786
|
const toolbar = toolbarRef.current;
|
|
747
787
|
if (!toolbar) {
|
|
748
|
-
|
|
788
|
+
setInlineToolbarFrame(null);
|
|
749
789
|
return;
|
|
750
790
|
}
|
|
751
791
|
toolbar.measureInWindow((x, y, width, height) => {
|
|
752
792
|
if (width <= 0 || height <= 0) {
|
|
753
|
-
|
|
793
|
+
setInlineToolbarFrame(null);
|
|
754
794
|
return;
|
|
755
795
|
}
|
|
756
|
-
const
|
|
757
|
-
|
|
796
|
+
const nextFrame = { x, y, width, height };
|
|
797
|
+
setInlineToolbarFrame((prev) => areToolbarFramesEqual(prev, nextFrame) ? prev : nextFrame);
|
|
758
798
|
});
|
|
759
799
|
}, []);
|
|
760
800
|
(0, react_1.useEffect)(() => {
|
|
761
801
|
if (!(showToolbar && toolbarPlacement === 'inline' && isFocused && editable)) {
|
|
762
|
-
|
|
802
|
+
setInlineToolbarFrame(null);
|
|
763
803
|
return;
|
|
764
804
|
}
|
|
765
805
|
const frame = requestAnimationFrame(() => {
|
|
@@ -836,19 +876,40 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
836
876
|
}
|
|
837
877
|
onSelectionChangeRef.current?.(nextSelection);
|
|
838
878
|
}, [syncSelectionStateFromUpdate]);
|
|
879
|
+
const refocusAfterToolbarInteraction = (0, react_1.useCallback)(() => {
|
|
880
|
+
nativeViewRef.current?.focus?.();
|
|
881
|
+
requestAnimationFrame(() => {
|
|
882
|
+
nativeViewRef.current?.focus?.();
|
|
883
|
+
});
|
|
884
|
+
setTimeout(() => {
|
|
885
|
+
nativeViewRef.current?.focus?.();
|
|
886
|
+
}, 50);
|
|
887
|
+
}, []);
|
|
839
888
|
const handleFocusChange = (0, react_1.useCallback)((event) => {
|
|
840
889
|
const { isFocused: focused } = event.nativeEvent;
|
|
890
|
+
if (!focused &&
|
|
891
|
+
editable &&
|
|
892
|
+
isFocusedRef.current &&
|
|
893
|
+
(0, EditorToolbar_1.isEditorToolbarFocusPreservationActive)()) {
|
|
894
|
+
setIsFocused(true);
|
|
895
|
+
refocusAfterToolbarInteraction();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const wasFocused = isFocusedRef.current;
|
|
899
|
+
isFocusedRef.current = focused;
|
|
841
900
|
setIsFocused(focused);
|
|
842
901
|
if (!focused) {
|
|
843
902
|
setMentionQueryEvent(null);
|
|
844
903
|
}
|
|
845
904
|
if (focused) {
|
|
846
|
-
|
|
905
|
+
if (!wasFocused) {
|
|
906
|
+
onFocusRef.current?.();
|
|
907
|
+
}
|
|
847
908
|
}
|
|
848
|
-
else {
|
|
909
|
+
else if (wasFocused) {
|
|
849
910
|
onBlurRef.current?.();
|
|
850
911
|
}
|
|
851
|
-
}, []);
|
|
912
|
+
}, [editable, refocusAfterToolbarInteraction]);
|
|
852
913
|
(0, react_1.useEffect)(() => {
|
|
853
914
|
if (addons?.mentions != null) {
|
|
854
915
|
return;
|
|
@@ -1130,6 +1191,13 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1130
1191
|
return '';
|
|
1131
1192
|
return bridgeRef.current.getHtml().replace(/<[^>]+>/g, '');
|
|
1132
1193
|
},
|
|
1194
|
+
async getCaretRect() {
|
|
1195
|
+
const nativeView = nativeViewRef.current;
|
|
1196
|
+
if (!nativeView?.getCaretRect)
|
|
1197
|
+
return null;
|
|
1198
|
+
const raw = await Promise.resolve(nativeView.getCaretRect());
|
|
1199
|
+
return parseCaretRectJson(raw);
|
|
1200
|
+
},
|
|
1133
1201
|
undo() {
|
|
1134
1202
|
runAndApply(() => bridgeRef.current?.undo() ?? null);
|
|
1135
1203
|
},
|
|
@@ -1219,6 +1287,14 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1219
1287
|
nativeViewStyleParts.push({ height: autoGrowHeight });
|
|
1220
1288
|
}
|
|
1221
1289
|
const nativeViewStyle = nativeViewStyleParts.length <= 1 ? nativeViewStyleParts[0] : nativeViewStyleParts;
|
|
1290
|
+
const toolbarFrameJson = serializeToolbarFrames(isFocused && editable
|
|
1291
|
+
? [
|
|
1292
|
+
...(toolbarPlacement === 'inline' && inlineToolbarFrame != null
|
|
1293
|
+
? [inlineToolbarFrame]
|
|
1294
|
+
: []),
|
|
1295
|
+
...registeredToolbarFrames,
|
|
1296
|
+
]
|
|
1297
|
+
: undefined);
|
|
1222
1298
|
const jsToolbar = ((0, jsx_runtime_1.jsx)(react_native_1.View, { ref: toolbarRef, testID: 'native-editor-js-toolbar', style: [
|
|
1223
1299
|
styles.inlineToolbar,
|
|
1224
1300
|
{ marginTop: inlineToolbarMarginTop },
|
|
@@ -1270,7 +1346,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1270
1346
|
'#8E8E93',
|
|
1271
1347
|
},
|
|
1272
1348
|
], children: suggestion.subtitle })) : null] })) }, suggestion.key));
|
|
1273
|
-
}) }) })) : ((0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: inlineToolbarShowTopBorder, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
|
|
1349
|
+
}) }) })) : ((0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: inlineToolbarShowTopBorder, preserveEditorFocus: false, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
|
|
1274
1350
|
skipNativeApplyIfContentUnchanged: true,
|
|
1275
1351
|
}), onToggleListType: (listType) => runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null), onToggleHeading: (level) => runAndApply(() => bridgeRef.current?.toggleHeading(level) ?? null), onToggleBlockquote: () => runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null), onInsertNodeType: (nodeType) => runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null), onRunCommand: (command) => {
|
|
1276
1352
|
switch (command) {
|
|
@@ -1296,7 +1372,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1296
1372
|
}), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
|
|
1297
1373
|
skipNativeApplyIfContentUnchanged: true,
|
|
1298
1374
|
}), onToggleBulletList: () => runAndApply(() => bridgeRef.current?.toggleList('bulletList') ?? null), onToggleOrderedList: () => runAndApply(() => bridgeRef.current?.toggleList('orderedList') ?? null), onIndentList: () => runAndApply(() => bridgeRef.current?.indentListItem() ?? null), onOutdentList: () => runAndApply(() => bridgeRef.current?.outdentListItem() ?? null), onInsertHorizontalRule: () => runAndApply(() => bridgeRef.current?.insertNode('horizontalRule') ?? null), onInsertLineBreak: () => runAndApply(() => bridgeRef.current?.insertNode('hardBreak') ?? null), onUndo: () => runAndApply(() => bridgeRef.current?.undo() ?? null), onRedo: () => runAndApply(() => bridgeRef.current?.redo() ?? null) })) }));
|
|
1299
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson:
|
|
1375
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarFrameJson, editorUpdateJson: pendingNativeUpdate.json, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
|
|
1300
1376
|
});
|
|
1301
1377
|
const styles = react_native_1.StyleSheet.create({
|
|
1302
1378
|
container: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
|
|
1
|
+
export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
|
|
2
2
|
export { NativeProseViewer, type NativeProseViewerProps, type NativeProseViewerAddons, type NativeProseViewerMentionsAddonConfig, type NativeProseViewerMentionPrefix, type NativeProseViewerLinkPressEvent, type NativeProseViewerMentionRenderContext, type NativeProseViewerMentionPressEvent, } from './NativeProseViewer';
|
|
3
3
|
export { EditorToolbar, DEFAULT_EDITOR_TOOLBAR_ITEMS, type EditorToolbarProps, type EditorToolbarItem, type EditorToolbarLeafItem, type EditorToolbarGroupChildItem, type EditorToolbarGroupItem, type EditorToolbarGroupPresentation, type EditorToolbarIcon, type EditorToolbarDefaultIconId, type EditorToolbarSFSymbolIcon, type EditorToolbarMaterialIcon, type EditorToolbarCommand, type EditorToolbarHeadingLevel, type EditorToolbarListType, } from './EditorToolbar';
|
|
4
4
|
export type { EditorContentInsets, EditorTheme, EditorTextStyle, EditorLinkTheme, EditorHeadingTheme, EditorListTheme, EditorHorizontalRuleTheme, EditorMentionTheme, EditorToolbarTheme, EditorToolbarAppearance, EditorFontStyle, EditorFontWeight, } from './EditorTheme';
|
|
@@ -8,32 +8,32 @@
|
|
|
8
8
|
<key>BinaryPath</key>
|
|
9
9
|
<string>libeditor_core.a</string>
|
|
10
10
|
<key>LibraryIdentifier</key>
|
|
11
|
-
<string>ios-
|
|
11
|
+
<string>ios-arm64</string>
|
|
12
12
|
<key>LibraryPath</key>
|
|
13
13
|
<string>libeditor_core.a</string>
|
|
14
14
|
<key>SupportedArchitectures</key>
|
|
15
15
|
<array>
|
|
16
16
|
<string>arm64</string>
|
|
17
|
-
<string>x86_64</string>
|
|
18
17
|
</array>
|
|
19
18
|
<key>SupportedPlatform</key>
|
|
20
19
|
<string>ios</string>
|
|
21
|
-
<key>SupportedPlatformVariant</key>
|
|
22
|
-
<string>simulator</string>
|
|
23
20
|
</dict>
|
|
24
21
|
<dict>
|
|
25
22
|
<key>BinaryPath</key>
|
|
26
23
|
<string>libeditor_core.a</string>
|
|
27
24
|
<key>LibraryIdentifier</key>
|
|
28
|
-
<string>ios-
|
|
25
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
29
26
|
<key>LibraryPath</key>
|
|
30
27
|
<string>libeditor_core.a</string>
|
|
31
28
|
<key>SupportedArchitectures</key>
|
|
32
29
|
<array>
|
|
33
30
|
<string>arm64</string>
|
|
31
|
+
<string>x86_64</string>
|
|
34
32
|
</array>
|
|
35
33
|
<key>SupportedPlatform</key>
|
|
36
34
|
<string>ios</string>
|
|
35
|
+
<key>SupportedPlatformVariant</key>
|
|
36
|
+
<string>simulator</string>
|
|
37
37
|
</dict>
|
|
38
38
|
</array>
|
|
39
39
|
<key>CFBundlePackageType</key>
|
|
Binary file
|
|
Binary file
|
|
@@ -1626,7 +1626,8 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1626
1626
|
inputViewStyle: .keyboard
|
|
1627
1627
|
)
|
|
1628
1628
|
private let accessoryPlaceholder = EditorAccessoryPlaceholderView(frame: .zero)
|
|
1629
|
-
private var
|
|
1629
|
+
private var toolbarFramesInWindow: [CGRect] = []
|
|
1630
|
+
private var lastToolbarTouchUptime: TimeInterval = -Double.infinity
|
|
1630
1631
|
private var didApplyAutoFocus = false
|
|
1631
1632
|
private var toolbarState = NativeToolbarState.empty
|
|
1632
1633
|
private var toolbarItems: [NativeToolbarItem] = NativeToolbarItem.defaults
|
|
@@ -1855,17 +1856,32 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1855
1856
|
lastToolbarFrameJSON = toolbarFrameJson
|
|
1856
1857
|
guard let toolbarFrameJson,
|
|
1857
1858
|
let data = toolbarFrameJson.data(using: .utf8),
|
|
1858
|
-
let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
1859
|
-
|
|
1859
|
+
let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
1860
|
+
else {
|
|
1861
|
+
toolbarFramesInWindow = []
|
|
1862
|
+
return
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if let frameDictionaries = raw["frames"] as? [[String: Any]] {
|
|
1866
|
+
toolbarFramesInWindow = frameDictionaries.compactMap(Self.toolbarFrame(from:))
|
|
1867
|
+
return
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
toolbarFramesInWindow = Self.toolbarFrame(from: raw).map { [$0] } ?? []
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
private static func toolbarFrame(from raw: [String: Any]) -> CGRect? {
|
|
1874
|
+
guard let x = (raw["x"] as? NSNumber)?.doubleValue,
|
|
1860
1875
|
let y = (raw["y"] as? NSNumber)?.doubleValue,
|
|
1861
1876
|
let width = (raw["width"] as? NSNumber)?.doubleValue,
|
|
1862
|
-
let height = (raw["height"] as? NSNumber)?.doubleValue
|
|
1877
|
+
let height = (raw["height"] as? NSNumber)?.doubleValue,
|
|
1878
|
+
width > 0,
|
|
1879
|
+
height > 0
|
|
1863
1880
|
else {
|
|
1864
|
-
|
|
1865
|
-
return
|
|
1881
|
+
return nil
|
|
1866
1882
|
}
|
|
1867
1883
|
|
|
1868
|
-
|
|
1884
|
+
return CGRect(x: x, y: y, width: width, height: height)
|
|
1869
1885
|
}
|
|
1870
1886
|
|
|
1871
1887
|
func setPendingEditorUpdateJson(_ editorUpdateJson: String?) {
|
|
@@ -1904,6 +1920,30 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1904
1920
|
richTextView.textView.resignFirstResponder()
|
|
1905
1921
|
}
|
|
1906
1922
|
|
|
1923
|
+
func getCaretRectJson() -> String? {
|
|
1924
|
+
layoutIfNeeded()
|
|
1925
|
+
richTextView.layoutIfNeeded()
|
|
1926
|
+
|
|
1927
|
+
guard let caretRect = richTextView.currentCaretRect() else {
|
|
1928
|
+
return nil
|
|
1929
|
+
}
|
|
1930
|
+
let editorRect = richTextView.convert(caretRect, to: self)
|
|
1931
|
+
let payload: [String: Any] = [
|
|
1932
|
+
"x": editorRect.minX,
|
|
1933
|
+
"y": editorRect.minY,
|
|
1934
|
+
"width": editorRect.width,
|
|
1935
|
+
"height": editorRect.height,
|
|
1936
|
+
"editorWidth": bounds.width,
|
|
1937
|
+
"editorHeight": bounds.height,
|
|
1938
|
+
]
|
|
1939
|
+
guard let data = try? JSONSerialization.data(withJSONObject: payload),
|
|
1940
|
+
let json = String(data: data, encoding: .utf8)
|
|
1941
|
+
else {
|
|
1942
|
+
return nil
|
|
1943
|
+
}
|
|
1944
|
+
return json
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1907
1947
|
// MARK: - Focus Notifications
|
|
1908
1948
|
|
|
1909
1949
|
@objc private func textViewDidBeginEditing(_ notification: Notification) {
|
|
@@ -1914,6 +1954,13 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1914
1954
|
}
|
|
1915
1955
|
|
|
1916
1956
|
@objc private func textViewDidEndEditing(_ notification: Notification) {
|
|
1957
|
+
if shouldPreserveFocusAfterToolbarTouch() {
|
|
1958
|
+
DispatchQueue.main.async { [weak self] in
|
|
1959
|
+
self?.richTextView.textView.becomeFirstResponder()
|
|
1960
|
+
}
|
|
1961
|
+
return
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1917
1964
|
uninstallOutsideTapRecognizer()
|
|
1918
1965
|
richTextView.textView.refreshSelectionVisualState()
|
|
1919
1966
|
clearMentionQueryStateAndHidePopover()
|
|
@@ -1952,6 +1999,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1952
1999
|
guard gestureRecognizer === outsideTapGestureRecognizer else { return true }
|
|
1953
2000
|
guard let tapWindow = gestureWindow ?? window else { return true }
|
|
1954
2001
|
let locationInWindow = touch.location(in: tapWindow)
|
|
2002
|
+
if isLocationInStandaloneToolbarFrame(locationInWindow) {
|
|
2003
|
+
markRecentToolbarTouch()
|
|
2004
|
+
}
|
|
1955
2005
|
let result = shouldHandleOutsideTap(
|
|
1956
2006
|
locationInWindow: locationInWindow,
|
|
1957
2007
|
touchedView: touch.view
|
|
@@ -1959,6 +2009,18 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1959
2009
|
return result
|
|
1960
2010
|
}
|
|
1961
2011
|
|
|
2012
|
+
private func markRecentToolbarTouch() {
|
|
2013
|
+
lastToolbarTouchUptime = ProcessInfo.processInfo.systemUptime
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
private func shouldPreserveFocusAfterToolbarTouch() -> Bool {
|
|
2017
|
+
ProcessInfo.processInfo.systemUptime - lastToolbarTouchUptime <= 0.75
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
private func isLocationInStandaloneToolbarFrame(_ locationInWindow: CGPoint) -> Bool {
|
|
2021
|
+
toolbarFramesInWindow.contains(where: { $0.contains(locationInWindow) })
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1962
2024
|
private func shouldHandleOutsideTap(
|
|
1963
2025
|
locationInWindow: CGPoint,
|
|
1964
2026
|
touchedView: UIView?
|
|
@@ -1975,7 +2037,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1975
2037
|
if let touchedView, touchedView.isDescendant(of: accessoryToolbar) {
|
|
1976
2038
|
return false
|
|
1977
2039
|
}
|
|
1978
|
-
if
|
|
2040
|
+
if isLocationInStandaloneToolbarFrame(locationInWindow) {
|
|
1979
2041
|
return false
|
|
1980
2042
|
}
|
|
1981
2043
|
return true
|
|
@@ -386,6 +386,9 @@ public class NativeEditorModule: Module {
|
|
|
386
386
|
AsyncFunction("blur") { (view: NativeEditorExpoView) in
|
|
387
387
|
view.blur()
|
|
388
388
|
}
|
|
389
|
+
AsyncFunction("getCaretRect") { (view: NativeEditorExpoView) -> String? in
|
|
390
|
+
view.getCaretRectJson()
|
|
391
|
+
}
|
|
389
392
|
}
|
|
390
393
|
|
|
391
394
|
View(NativeProseViewerExpoView.self) {
|
|
@@ -4669,6 +4669,13 @@ final class RichTextEditorView: UIView {
|
|
|
4669
4669
|
remoteSelectionOverlayView.refresh()
|
|
4670
4670
|
}
|
|
4671
4671
|
|
|
4672
|
+
func currentCaretRect() -> CGRect? {
|
|
4673
|
+
guard let selectedTextRange = textView.selectedTextRange else { return nil }
|
|
4674
|
+
let rect = textView.caretRect(for: selectedTextRange.end)
|
|
4675
|
+
guard rect.height > 0 else { return nil }
|
|
4676
|
+
return textView.convert(rect, to: self)
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4672
4679
|
func remoteSelectionOverlaySubviewsForTesting() -> [UIView] {
|
|
4673
4680
|
remoteSelectionOverlayView.subviews.filter { !$0.isHidden }
|
|
4674
4681
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollohg/react-native-prose-editor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "Native rich text editor with Rust core for React Native",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/apollohg/react-native-prose-editor",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|