@apollohg/react-native-rich-text-editor 2.0.2 → 2.0.4
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 +21 -7
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +20 -6
- package/android/src/main/java/com/apollohg/editor/EditorEditTextClipboard.kt +181 -36
- package/android/src/main/java/com/apollohg/editor/EditorEditTextSelection.kt +7 -0
- package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceInteraction.kt +31 -11
- package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +36 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Backend.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +10 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +2 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewProperties.kt +4 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +3 -0
- package/android/src/main/java/com/apollohg/editor/UniffiEditorV2Backend.kt +4 -0
- package/dist/NativeRichTextEditor.d.ts +1 -1
- package/dist/RichTextEditorNativeTypes.d.ts +2 -1
- package/dist/RichTextEditorTypes.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/useRichTextEditorPresentation.d.ts +1 -1
- package/dist/useRichTextEditorPresentation.js +2 -2
- package/dist/useRichTextEditorState.d.ts +5 -3
- package/dist/useRichTextEditorState.js +4 -3
- 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/EditorTextView+KeyboardInput.swift +299 -0
- package/ios/EditorTextView+Selection.swift +5 -2
- package/ios/EditorTextView.swift +63 -43
- package/ios/EditorV2Adapter+Commands.swift +22 -0
- package/ios/EditorV2Shadow.swift +12 -0
- package/ios/Generated_editor_core.swift +10 -0
- package/ios/NativeEditorExpoView+Properties.swift +4 -0
- package/ios/NativeEditorModule.swift +3 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +11 -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/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +18 -0
- package/src/NativeRichTextEditor.tsx +1 -0
- package/src/RichTextEditorNativeTypes.tsx +2 -0
- package/src/RichTextEditorTypes.tsx +5 -0
- package/src/index.ts +1 -0
- package/src/useRichTextEditorPresentation.tsx +3 -0
- package/src/useRichTextEditorState.tsx +5 -2
package/README.md
CHANGED
|
@@ -8,13 +8,15 @@ See the [documentation](https://github.com/apollohg/react-native-rich-text-edito
|
|
|
8
8
|
|
|
9
9
|
## Highlights
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
**⭐ Native editing on iOS and Android**
|
|
12
|
+
**⭐ Built-in Yjs collaboration**
|
|
13
|
+
**⭐ React Native components inside documents**
|
|
14
|
+
**⭐ External text composition**
|
|
15
|
+
**⭐ Dedicated Fabric viewer**
|
|
16
|
+
**⭐ Configurable document schemas** with HTML and ProseMirror JSON import/export
|
|
17
|
+
**⭐ Nested bullet and numbered lists**
|
|
18
|
+
**⭐ Optional native code syntax highlighting**
|
|
19
|
+
**⭐ Rich copy and paste**
|
|
18
20
|
|
|
19
21
|
## Requirements
|
|
20
22
|
|
|
@@ -87,6 +89,18 @@ export function EditorScreen() {
|
|
|
87
89
|
|
|
88
90
|
See [Getting Started](https://github.com/apollohg/react-native-rich-text-editor/wiki/Getting-Started) and the [RichTextEditor reference](https://github.com/apollohg/react-native-rich-text-editor/wiki/RichTextEditor-Reference) for the complete API.
|
|
89
91
|
|
|
92
|
+
## Copy and paste
|
|
93
|
+
|
|
94
|
+
Copy and cut publish an editor fragment, HTML, and plain text. Pasting between compatible editor schemas preserves selected marks, nodes, and atom attributes. External rich text imports supported HTML formatting and structure, with RTF conversion on iOS.
|
|
95
|
+
|
|
96
|
+
Use `pasteMode` to control native clipboard paste:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<RichTextEditor documentHandle={documentHandle} pasteMode="plainText" />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The default is `"rich"`. `"plainText"` removes source formatting and atoms, while retaining the destination's ordinary typing marks. `"disabled"` blocks paste without disabling copy or typing. Changes apply immediately. These settings govern clipboard actions; programmatic insertion and drag/drop retain their own behaviour. Unsupported external formatting is reduced to supported content, and clipboard images do not trigger uploads.
|
|
103
|
+
|
|
90
104
|
## Custom atom nodes
|
|
91
105
|
|
|
92
106
|
Render interactive cards, embeds, and other custom blocks with your own React components.
|
|
@@ -133,6 +133,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
internal var pasteMode: EditorPasteMode = EditorPasteMode.RICH
|
|
137
|
+
|
|
136
138
|
/**
|
|
137
139
|
* Guard flag to prevent re-entrant input interception while we're
|
|
138
140
|
* applying state from Rust (calling [setText] or modifying text storage).
|
|
@@ -245,6 +247,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
245
247
|
internal var currentRenderBlocksNeedFullApply = false
|
|
246
248
|
internal var authorizedVisibleTextNeedsRebuild = false
|
|
247
249
|
internal var logicalSelectionSnapshot: LogicalSelectionSnapshot? = null
|
|
250
|
+
internal var authoritativeNodeSelectionRange: ImageSelectionRange? = null
|
|
248
251
|
internal var lastAllowedAtomCaretSelection: Pair<Int, Int>? = null
|
|
249
252
|
private var localTextDrag: LocalTextDrag? = null
|
|
250
253
|
internal var lastAppliedDocumentVersion: String? = null
|
|
@@ -275,6 +278,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
275
278
|
internal var onSetSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
276
279
|
internal var onDeleteAndSplitScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
277
280
|
internal var onInsertContentHtmlInRustForTesting: ((String) -> Unit)? = null
|
|
281
|
+
internal var onSetPrimaryClipForTesting: ((android.content.ClipData) -> Unit)? = null
|
|
278
282
|
internal var onResizeImageAtDocPosForTesting: ((Int, Int, Int) -> Unit)? = null
|
|
279
283
|
internal var onMoveSelectionScalarForTesting: ((Int, Int, Int) -> Unit)? = null
|
|
280
284
|
internal var onBeforeRenderRefresh: (() -> Unit)? = null
|
|
@@ -788,14 +792,18 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
788
792
|
|
|
789
793
|
fun performToolbarRedo() = performToolbarRedoImpl()
|
|
790
794
|
|
|
791
|
-
/**
|
|
792
|
-
* Intercept paste operations to route content through Rust.
|
|
793
|
-
*
|
|
794
|
-
* Attempts to extract HTML from the clipboard first (for rich text paste),
|
|
795
|
-
* falling back to plain text.
|
|
796
|
-
*/
|
|
795
|
+
/** Routes native clipboard actions through the authoritative Rust selection. */
|
|
797
796
|
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
797
|
+
if (id == android.R.id.copy) {
|
|
798
|
+
handleCopy()
|
|
799
|
+
return true
|
|
800
|
+
}
|
|
798
801
|
if (!isEditable && isMutatingContextMenuItem(id)) return true
|
|
802
|
+
if (pasteMode == EditorPasteMode.DISABLED &&
|
|
803
|
+
(id == android.R.id.paste || id == android.R.id.pasteAsPlainText)
|
|
804
|
+
) {
|
|
805
|
+
return true
|
|
806
|
+
}
|
|
799
807
|
if (id == android.R.id.cut) {
|
|
800
808
|
handleCut()
|
|
801
809
|
return true
|
|
@@ -812,6 +820,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
812
820
|
* Selection and copy actions remain available.
|
|
813
821
|
*/
|
|
814
822
|
override fun performAccessibilityAction(action: Int, arguments: android.os.Bundle?): Boolean {
|
|
823
|
+
if (pasteMode == EditorPasteMode.DISABLED &&
|
|
824
|
+
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_PASTE
|
|
825
|
+
) {
|
|
826
|
+
return false
|
|
827
|
+
}
|
|
815
828
|
if (!isEditable && (
|
|
816
829
|
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT ||
|
|
817
830
|
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_PASTE ||
|
|
@@ -842,6 +855,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
842
855
|
}
|
|
843
856
|
ensureSelectionVisible()
|
|
844
857
|
if (isApplyingRustState) return
|
|
858
|
+
authoritativeNodeSelectionRange = null
|
|
845
859
|
val wasExternallyComposing = externalTextComposition != null
|
|
846
860
|
if (!commitExternalTextCompositionBeforeInteractionIfNeeded()) return
|
|
847
861
|
if (wasExternallyComposing) {
|
|
@@ -1,9 +1,94 @@
|
|
|
1
1
|
package com.apollohg.editor
|
|
2
2
|
|
|
3
3
|
import android.content.ClipData
|
|
4
|
+
import android.content.ClipDescription
|
|
4
5
|
import android.content.ClipboardManager
|
|
5
6
|
import android.content.Context
|
|
6
|
-
import android.
|
|
7
|
+
import android.os.PersistableBundle
|
|
8
|
+
import org.json.JSONObject
|
|
9
|
+
|
|
10
|
+
internal enum class EditorPasteMode {
|
|
11
|
+
RICH,
|
|
12
|
+
PLAIN_TEXT,
|
|
13
|
+
DISABLED;
|
|
14
|
+
|
|
15
|
+
companion object {
|
|
16
|
+
fun fromRaw(value: String?): EditorPasteMode = when (value) {
|
|
17
|
+
"plainText" -> PLAIN_TEXT
|
|
18
|
+
"disabled" -> DISABLED
|
|
19
|
+
else -> RICH
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
internal data class EditorClipboardPayload(
|
|
25
|
+
val fragment: String? = null,
|
|
26
|
+
val html: String? = null,
|
|
27
|
+
val text: String? = null
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
internal object EditorClipboard {
|
|
31
|
+
const val MIME_TYPE_FRAGMENT = "application/vnd.apollohg.native-editor.fragment+json"
|
|
32
|
+
const val EXTRA_FRAGMENT = "com.apollohg.editor.clipboard.FRAGMENT"
|
|
33
|
+
|
|
34
|
+
fun fromExportJson(json: String): EditorClipboardPayload? = try {
|
|
35
|
+
val value = JSONObject(json)
|
|
36
|
+
if (value.optBoolean("empty", false)) {
|
|
37
|
+
null
|
|
38
|
+
} else {
|
|
39
|
+
val fragment = value.optString("fragment").takeIf { it.isNotEmpty() }
|
|
40
|
+
val html = value.optString("html").takeIf { it.isNotEmpty() }
|
|
41
|
+
val text = value.optString("text").takeIf { it.isNotEmpty() }
|
|
42
|
+
if (fragment == null && html == null && text == null) {
|
|
43
|
+
null
|
|
44
|
+
} else {
|
|
45
|
+
EditorClipboardPayload(fragment, html, text)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (_: Exception) {
|
|
49
|
+
null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun create(payload: EditorClipboardPayload): ClipData {
|
|
53
|
+
val mimeTypes = buildList {
|
|
54
|
+
if (payload.html != null) add(ClipDescription.MIMETYPE_TEXT_HTML)
|
|
55
|
+
add(ClipDescription.MIMETYPE_TEXT_PLAIN)
|
|
56
|
+
if (payload.fragment != null) add(MIME_TYPE_FRAGMENT)
|
|
57
|
+
}.distinct().toTypedArray()
|
|
58
|
+
val description = ClipDescription("rich text", mimeTypes)
|
|
59
|
+
payload.fragment?.let { fragment ->
|
|
60
|
+
description.extras = PersistableBundle().apply {
|
|
61
|
+
putString(EXTRA_FRAGMENT, fragment)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return ClipData(
|
|
65
|
+
description,
|
|
66
|
+
ClipData.Item(payload.text ?: "", payload.html, null, null)
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun read(clip: ClipData, context: Context): EditorClipboardPayload {
|
|
71
|
+
if (clip.itemCount == 0) return EditorClipboardPayload()
|
|
72
|
+
val item = clip.getItemAt(0)
|
|
73
|
+
val hasPrivateMime = (0 until clip.description.mimeTypeCount).any {
|
|
74
|
+
clip.description.getMimeType(it) == MIME_TYPE_FRAGMENT
|
|
75
|
+
}
|
|
76
|
+
val fragment = if (hasPrivateMime) {
|
|
77
|
+
runCatching {
|
|
78
|
+
clip.description.extras?.getString(EXTRA_FRAGMENT)
|
|
79
|
+
}.getOrNull()
|
|
80
|
+
} else {
|
|
81
|
+
null
|
|
82
|
+
}
|
|
83
|
+
return EditorClipboardPayload(
|
|
84
|
+
fragment = fragment,
|
|
85
|
+
html = item.htmlText,
|
|
86
|
+
text = item.text?.toString() ?: runCatching {
|
|
87
|
+
item.coerceToText(context)?.toString()
|
|
88
|
+
}.getOrNull()
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
7
92
|
|
|
8
93
|
internal fun EditorEditText.isMutatingContextMenuItem(id: Int): Boolean =
|
|
9
94
|
id == android.R.id.paste ||
|
|
@@ -14,16 +99,37 @@ internal fun EditorEditText.prepareForExternalInteractionMutation(): Boolean =
|
|
|
14
99
|
commitExternalTextCompositionBeforeInteractionIfNeeded() &&
|
|
15
100
|
prepareForExternalEditorUpdate()
|
|
16
101
|
|
|
17
|
-
internal fun EditorEditText.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
102
|
+
internal fun EditorEditText.publishClipboard(payload: EditorClipboardPayload): Boolean {
|
|
103
|
+
return try {
|
|
104
|
+
val clip = EditorClipboard.create(payload)
|
|
105
|
+
val testWriter = onSetPrimaryClipForTesting
|
|
106
|
+
if (testWriter != null) {
|
|
107
|
+
testWriter(clip)
|
|
108
|
+
} else {
|
|
109
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
110
|
+
?: return false
|
|
111
|
+
clipboard.setPrimaryClip(clip)
|
|
112
|
+
}
|
|
113
|
+
true
|
|
114
|
+
} catch (_: Exception) {
|
|
115
|
+
false
|
|
23
116
|
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
internal fun EditorEditText.handleCopy(): Boolean {
|
|
120
|
+
if (editorId == 0L || v2Driver == null) return baseTextContextMenuItem(android.R.id.copy)
|
|
121
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
122
|
+
if (!prepareForExternalInteractionMutation()) return false
|
|
123
|
+
syncCurrentSelectionToRust()
|
|
124
|
+
val payload = v2Driver?.clipboardJson()?.let(EditorClipboard::fromExportJson) ?: return false
|
|
125
|
+
return publishClipboard(payload)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
internal fun EditorEditText.handlePaste(plainTextOnly: Boolean) {
|
|
129
|
+
val forcePlainText = plainTextOnly || pasteMode == EditorPasteMode.PLAIN_TEXT
|
|
24
130
|
if (editorId == 0L) {
|
|
25
131
|
baseTextContextMenuItem(
|
|
26
|
-
if (
|
|
132
|
+
if (forcePlainText) android.R.id.pasteAsPlainText else android.R.id.paste
|
|
27
133
|
)
|
|
28
134
|
return
|
|
29
135
|
}
|
|
@@ -32,21 +138,40 @@ internal fun EditorEditText.handlePaste(plainTextOnly: Boolean) {
|
|
|
32
138
|
|
|
33
139
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
34
140
|
?: return
|
|
35
|
-
val clip = clipboard.primaryClip ?: return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
val item = clip.getItemAt(0)
|
|
141
|
+
val clip = runCatching { clipboard.primaryClip }.getOrNull() ?: return
|
|
142
|
+
val payload = EditorClipboard.read(clip, context)
|
|
143
|
+
if (payload.fragment == null && payload.html == null && payload.text.isNullOrEmpty()) return
|
|
39
144
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
145
|
+
if (payload.fragment == null && payload.html == null &&
|
|
146
|
+
(onInsertTextInRustForTesting != null || onReplaceTextInRustForTesting != null)
|
|
147
|
+
) {
|
|
148
|
+
payload.text?.let(::pastePlainText)
|
|
43
149
|
return
|
|
44
150
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
151
|
+
val driver = v2Driver
|
|
152
|
+
if (driver == null) {
|
|
153
|
+
if (forcePlainText) {
|
|
154
|
+
payload.text?.let(::pastePlainText)
|
|
155
|
+
} else if (payload.html != null) {
|
|
156
|
+
pasteHTML(payload.html)
|
|
157
|
+
} else {
|
|
158
|
+
payload.text?.let(::pastePlainText)
|
|
159
|
+
}
|
|
160
|
+
return
|
|
49
161
|
}
|
|
162
|
+
val (anchor, head) = currentScalarSelection() ?: return
|
|
163
|
+
val preserveEngineSelection = authoritativeNodeSelectionRange?.let { range ->
|
|
164
|
+
selectionStart == range.start && selectionEnd == range.end
|
|
165
|
+
} == true
|
|
166
|
+
driver.pasteAtSelection(
|
|
167
|
+
fragment = payload.fragment,
|
|
168
|
+
html = payload.html,
|
|
169
|
+
text = payload.text,
|
|
170
|
+
plainText = forcePlainText,
|
|
171
|
+
anchor = anchor,
|
|
172
|
+
head = head,
|
|
173
|
+
preserveEngineSelection = preserveEngineSelection
|
|
174
|
+
)?.let { applyUpdateJSON(it) }
|
|
50
175
|
}
|
|
51
176
|
|
|
52
177
|
internal fun EditorEditText.handleCut() {
|
|
@@ -56,25 +181,45 @@ internal fun EditorEditText.handleCut() {
|
|
|
56
181
|
}
|
|
57
182
|
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
58
183
|
if (!prepareForExternalInteractionMutation()) return
|
|
184
|
+
if (v2Driver == null) {
|
|
185
|
+
val currentText = text?.toString() ?: return
|
|
186
|
+
val (selectionStart, selectionEnd) = normalizedUtf16SelectionRange(currentText) ?: return
|
|
187
|
+
val (utf16Start, utf16End) = PositionBridge.snapRangeToScalarBoundaries(
|
|
188
|
+
selectionStart,
|
|
189
|
+
selectionEnd,
|
|
190
|
+
currentText
|
|
191
|
+
)
|
|
192
|
+
if (utf16Start >= utf16End) return
|
|
193
|
+
val selectedText = currentText.substring(utf16Start, utf16End)
|
|
194
|
+
if (!publishClipboard(EditorClipboardPayload(text = selectedText))) return
|
|
195
|
+
deleteRangeInRust(
|
|
196
|
+
PositionBridge.utf16ToScalar(utf16Start, currentText),
|
|
197
|
+
PositionBridge.utf16ToScalar(utf16End, currentText)
|
|
198
|
+
)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
syncCurrentSelectionToRust()
|
|
59
202
|
|
|
60
|
-
val
|
|
61
|
-
|
|
62
|
-
if (selectionStart == selectionEnd) return
|
|
63
|
-
|
|
64
|
-
val (utf16Start, utf16End) = PositionBridge.snapRangeToScalarBoundaries(
|
|
65
|
-
selectionStart,
|
|
66
|
-
selectionEnd,
|
|
67
|
-
currentText
|
|
68
|
-
)
|
|
69
|
-
if (utf16Start >= utf16End) return
|
|
70
|
-
|
|
71
|
-
val selectedText = currentText.substring(utf16Start, utf16End)
|
|
72
|
-
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
73
|
-
clipboard?.setPrimaryClip(ClipData.newPlainText(null, selectedText))
|
|
203
|
+
val payload = v2Driver?.clipboardJson()?.let(EditorClipboard::fromExportJson) ?: return
|
|
204
|
+
if (!publishClipboard(payload)) return
|
|
74
205
|
|
|
75
|
-
val
|
|
76
|
-
|
|
77
|
-
|
|
206
|
+
val (anchor, head) = currentScalarSelection() ?: return
|
|
207
|
+
if (onDeleteRangeInRustForTesting != null) {
|
|
208
|
+
deleteRangeInRust(minOf(anchor, head), maxOf(anchor, head))
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
val preserveEngineSelection = authoritativeNodeSelectionRange?.let { range ->
|
|
212
|
+
selectionStart == range.start && selectionEnd == range.end
|
|
213
|
+
} == true
|
|
214
|
+
v2Driver?.pasteAtSelection(
|
|
215
|
+
fragment = null,
|
|
216
|
+
html = null,
|
|
217
|
+
text = "",
|
|
218
|
+
plainText = true,
|
|
219
|
+
anchor = anchor,
|
|
220
|
+
head = head,
|
|
221
|
+
preserveEngineSelection = preserveEngineSelection
|
|
222
|
+
)?.let { applyUpdateJSON(it) }
|
|
78
223
|
}
|
|
79
224
|
|
|
80
225
|
internal fun EditorEditText.handleAccessibilitySetText(arguments: android.os.Bundle?): Boolean {
|
|
@@ -100,6 +100,10 @@ internal fun EditorEditText.canonicalListCaretOffset(selStart: Int, selEnd: Int)
|
|
|
100
100
|
|
|
101
101
|
internal fun EditorEditText.syncCurrentSelectionToRust() {
|
|
102
102
|
if (!hasLiveEditor()) return
|
|
103
|
+
authoritativeNodeSelectionRange?.let { range ->
|
|
104
|
+
if (selectionStart == range.start && selectionEnd == range.end) return
|
|
105
|
+
authoritativeNodeSelectionRange = null
|
|
106
|
+
}
|
|
103
107
|
|
|
104
108
|
val currentText = text?.toString() ?: ""
|
|
105
109
|
if (currentText != lastAuthorizedText) return
|
|
@@ -296,6 +300,7 @@ internal fun EditorEditText.applySelectionFromJSON(
|
|
|
296
300
|
val currentText = text?.toString() ?: ""
|
|
297
301
|
when (type) {
|
|
298
302
|
"text" -> {
|
|
303
|
+
authoritativeNodeSelectionRange = null
|
|
299
304
|
val docAnchor = exactV2ScalarInt(selection.opt("anchor") as? Number) ?: return
|
|
300
305
|
val docHead = exactV2ScalarInt(selection.opt("head") as? Number) ?: return
|
|
301
306
|
// The frozen v2 update includes exact scalar positions alongside its
|
|
@@ -339,10 +344,12 @@ internal fun EditorEditText.applySelectionFromJSON(
|
|
|
339
344
|
val clamped = startUtf16.coerceIn(0, len)
|
|
340
345
|
val endClamped = (clamped + 1).coerceAtMost(len)
|
|
341
346
|
setSelection(clamped, endClamped)
|
|
347
|
+
authoritativeNodeSelectionRange = ImageSelectionRange(clamped, endClamped)
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
"all" -> {
|
|
345
351
|
logicalSelectionSnapshot = null
|
|
352
|
+
authoritativeNodeSelectionRange = null
|
|
346
353
|
selectAll()
|
|
347
354
|
}
|
|
348
355
|
}
|
|
@@ -391,21 +391,30 @@ internal class EditorTextSurfaceInteraction(
|
|
|
391
391
|
0,
|
|
392
392
|
android.R.string.copy
|
|
393
393
|
).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
|
|
394
|
-
|
|
394
|
+
val editor = view as? EditorEditText
|
|
395
|
+
if (editor?.isEditable != false) {
|
|
395
396
|
menu.add(
|
|
396
397
|
0,
|
|
397
398
|
android.R.id.cut,
|
|
398
399
|
1,
|
|
399
400
|
android.R.string.cut
|
|
400
401
|
).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
402
|
+
if (editor?.pasteMode != EditorPasteMode.DISABLED) {
|
|
403
|
+
menu.add(
|
|
404
|
+
0,
|
|
405
|
+
android.R.id.paste,
|
|
406
|
+
2,
|
|
407
|
+
android.R.string.paste
|
|
408
|
+
).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
|
|
409
|
+
menu.add(
|
|
410
|
+
0,
|
|
411
|
+
android.R.id.pasteAsPlainText,
|
|
412
|
+
3,
|
|
413
|
+
android.R.string.paste_as_plain_text
|
|
414
|
+
)
|
|
415
|
+
}
|
|
407
416
|
}
|
|
408
|
-
menu.add(0, android.R.id.selectAll,
|
|
417
|
+
menu.add(0, android.R.id.selectAll, 4, android.R.string.selectAll)
|
|
409
418
|
return true
|
|
410
419
|
}
|
|
411
420
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = false
|
|
@@ -457,12 +466,20 @@ internal class EditorTextSurfaceInteraction(
|
|
|
457
466
|
}
|
|
458
467
|
|
|
459
468
|
fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
|
460
|
-
if (event.isCtrlPressed) {
|
|
469
|
+
if (event.isCtrlPressed || event.isMetaPressed) {
|
|
461
470
|
val action = when (keyCode) {
|
|
462
471
|
KeyEvent.KEYCODE_A -> android.R.id.selectAll
|
|
472
|
+
|
|
463
473
|
KeyEvent.KEYCODE_C -> android.R.id.copy
|
|
474
|
+
|
|
464
475
|
KeyEvent.KEYCODE_X -> android.R.id.cut
|
|
465
|
-
|
|
476
|
+
|
|
477
|
+
KeyEvent.KEYCODE_V -> if (event.isShiftPressed) {
|
|
478
|
+
android.R.id.pasteAsPlainText
|
|
479
|
+
} else {
|
|
480
|
+
android.R.id.paste
|
|
481
|
+
}
|
|
482
|
+
|
|
466
483
|
else -> 0
|
|
467
484
|
}
|
|
468
485
|
if (action != 0) return view.onTextContextMenuItem(action)
|
|
@@ -559,10 +576,13 @@ internal class EditorTextSurfaceInteraction(
|
|
|
559
576
|
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY)
|
|
560
577
|
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY)
|
|
561
578
|
info.addAction(AccessibilityNodeInfo.ACTION_COPY)
|
|
579
|
+
val editor = view as? EditorEditText
|
|
562
580
|
if (info.isEditable) {
|
|
563
581
|
info.addAction(AccessibilityNodeInfo.ACTION_SET_TEXT)
|
|
564
582
|
info.addAction(AccessibilityNodeInfo.ACTION_CUT)
|
|
565
|
-
|
|
583
|
+
if (editor?.pasteMode != EditorPasteMode.DISABLED) {
|
|
584
|
+
info.addAction(AccessibilityNodeInfo.ACTION_PASTE)
|
|
585
|
+
}
|
|
566
586
|
}
|
|
567
587
|
}
|
|
568
588
|
|
|
@@ -796,6 +796,42 @@ internal class EditorV2Adapter private constructor(
|
|
|
796
796
|
)
|
|
797
797
|
}
|
|
798
798
|
|
|
799
|
+
override fun clipboardJson(): String? {
|
|
800
|
+
if (destroyed) return null
|
|
801
|
+
return when (val result = backend.getClipboard(editorId)) {
|
|
802
|
+
is EditorV2CallResult.Ok -> result.value
|
|
803
|
+
|
|
804
|
+
is EditorV2CallResult.Err -> {
|
|
805
|
+
emit(result.error)
|
|
806
|
+
null
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
override fun pasteAtSelection(
|
|
812
|
+
fragment: String?,
|
|
813
|
+
html: String?,
|
|
814
|
+
text: String?,
|
|
815
|
+
plainText: Boolean,
|
|
816
|
+
anchor: Int,
|
|
817
|
+
head: Int,
|
|
818
|
+
preserveEngineSelection: Boolean
|
|
819
|
+
): String? {
|
|
820
|
+
val command = JSONObject().put("type", "paste")
|
|
821
|
+
fragment?.let { command.put("fragment", it) }
|
|
822
|
+
html?.let { command.put("html", it) }
|
|
823
|
+
text?.let { command.put("text", it) }
|
|
824
|
+
if (plainText) command.put("plainText", true)
|
|
825
|
+
return performMutation(
|
|
826
|
+
preSelection = if (preserveEngineSelection) null else intArrayOf(anchor, head),
|
|
827
|
+
adoptEngineSelection = true
|
|
828
|
+
) {
|
|
829
|
+
callWithEnvelope(JSONObject().put("command", command)) { requestJson ->
|
|
830
|
+
backend.applyCommand(editorId, requestJson)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
799
835
|
override fun toggleMark(markName: String, anchor: Int, head: Int): String? = commandAtSelection(
|
|
800
836
|
JSONObject().put("type", "toggleMark").put("markType", markName),
|
|
801
837
|
anchor,
|
|
@@ -37,6 +37,7 @@ internal interface EditorV2Backend {
|
|
|
37
37
|
fun getDocumentJson(editorId: String): EditorV2CallResult<String>
|
|
38
38
|
fun getDocumentHtml(editorId: String): EditorV2CallResult<String>
|
|
39
39
|
fun getContentSnapshot(editorId: String): EditorV2CallResult<String>
|
|
40
|
+
fun getClipboard(editorId: String): EditorV2CallResult<String>
|
|
40
41
|
fun applyInput(editorId: String, requestJson: String): EditorV2CallResult<String>
|
|
41
42
|
fun applyCommand(editorId: String, requestJson: String): EditorV2CallResult<String>
|
|
42
43
|
fun applyLocalApi(editorId: String, requestJson: String): EditorV2CallResult<String>
|
|
@@ -50,6 +50,16 @@ internal interface EditorV2Driver {
|
|
|
50
50
|
fun insertNode(nodeType: String, anchor: Int, head: Int): String?
|
|
51
51
|
fun insertContentHtmlAtSelection(html: String, anchor: Int, head: Int): String?
|
|
52
52
|
fun insertContentJsonAtSelection(json: String, anchor: Int, head: Int): String?
|
|
53
|
+
fun clipboardJson(): String?
|
|
54
|
+
fun pasteAtSelection(
|
|
55
|
+
fragment: String?,
|
|
56
|
+
html: String?,
|
|
57
|
+
text: String?,
|
|
58
|
+
plainText: Boolean,
|
|
59
|
+
anchor: Int,
|
|
60
|
+
head: Int,
|
|
61
|
+
preserveEngineSelection: Boolean = false
|
|
62
|
+
): String?
|
|
53
63
|
fun toggleMark(markName: String, anchor: Int, head: Int): String?
|
|
54
64
|
fun setMark(markName: String, attrsJson: String, anchor: Int, head: Int): String?
|
|
55
65
|
fun unsetMark(markName: String, anchor: Int, head: Int): String?
|
|
@@ -388,6 +388,8 @@ class NativeEditorExpoView(context: Context, appContext: AppContext) :
|
|
|
388
388
|
|
|
389
389
|
fun setEditable(editable: Boolean) = setEditableImpl(editable)
|
|
390
390
|
|
|
391
|
+
fun setPasteMode(rawPasteMode: String?) = setPasteModeImpl(rawPasteMode)
|
|
392
|
+
|
|
391
393
|
fun beginExternalTextComposition(sessionId: String): String =
|
|
392
394
|
beginExternalTextCompositionImpl(sessionId)
|
|
393
395
|
|
|
@@ -140,6 +140,10 @@ internal fun NativeEditorExpoView.setEditableImpl(editable: Boolean) {
|
|
|
140
140
|
updateKeyboardToolbarVisibility()
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
internal fun NativeEditorExpoView.setPasteModeImpl(rawPasteMode: String?) {
|
|
144
|
+
richTextView.editorEditText.pasteMode = EditorPasteMode.fromRaw(rawPasteMode)
|
|
145
|
+
}
|
|
146
|
+
|
|
143
147
|
internal fun NativeEditorExpoView.setAccessibilityLabelImpl(label: String?) {
|
|
144
148
|
richTextView.editorEditText.contentDescription = label
|
|
145
149
|
}
|
|
@@ -520,6 +520,9 @@ class NativeEditorModule : Module() {
|
|
|
520
520
|
Prop("editable") { view: NativeEditorExpoView, editable: Boolean ->
|
|
521
521
|
view.setEditable(editable)
|
|
522
522
|
}
|
|
523
|
+
Prop("pasteMode") { view: NativeEditorExpoView, pasteMode: String? ->
|
|
524
|
+
view.setPasteMode(pasteMode)
|
|
525
|
+
}
|
|
523
526
|
Prop("accessibilityLabel") { view: NativeEditorExpoView, label: String? ->
|
|
524
527
|
view.setAccessibilityLabel(label)
|
|
525
528
|
}
|
|
@@ -20,6 +20,7 @@ import uniffi.editor_core.editorV2CollaborationSocketOpen
|
|
|
20
20
|
import uniffi.editor_core.editorV2Create
|
|
21
21
|
import uniffi.editor_core.editorV2Destroy
|
|
22
22
|
import uniffi.editor_core.editorV2DocToScalar
|
|
23
|
+
import uniffi.editor_core.editorV2GetClipboard
|
|
23
24
|
import uniffi.editor_core.editorV2GetContentSnapshot
|
|
24
25
|
import uniffi.editor_core.editorV2GetDocumentHtml
|
|
25
26
|
import uniffi.editor_core.editorV2GetDocumentJson
|
|
@@ -77,6 +78,9 @@ internal object UniffiEditorV2Backend : EditorV2Backend {
|
|
|
77
78
|
override fun getContentSnapshot(editorId: String): EditorV2CallResult<String> =
|
|
78
79
|
normalize(editorV2GetContentSnapshot(editorId))
|
|
79
80
|
|
|
81
|
+
override fun getClipboard(editorId: String): EditorV2CallResult<String> =
|
|
82
|
+
normalize(editorV2GetClipboard(editorId))
|
|
83
|
+
|
|
80
84
|
override fun applyInput(editorId: String, requestJson: String): EditorV2CallResult<String> =
|
|
81
85
|
normalize(editorV2ApplyInput(editorId, requestJson))
|
|
82
86
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { type RichTextEditorHeightBehavior, type RichTextEditorToolbarPlacement, type RichTextEditorValueJSONUpdateMode, type RichTextEditorAutoCapitalize, type RichTextEditorKeyboardType, type RichTextEditorAndroidInputOptions, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, type RichTextEditorProps, type RichTextEditorRef, type RichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type NativeRichTextEditorValueJSONUpdateMode, type NativeRichTextEditorAutoCapitalize, type NativeRichTextEditorKeyboardType, type NativeRichTextEditorAndroidInputOptions, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, } from './RichTextEditorTypes';
|
|
1
|
+
export { type RichTextEditorHeightBehavior, type RichTextEditorToolbarPlacement, type RichTextEditorValueJSONUpdateMode, type RichTextEditorPasteMode, type RichTextEditorAutoCapitalize, type RichTextEditorKeyboardType, type RichTextEditorAndroidInputOptions, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, type RichTextEditorProps, type RichTextEditorRef, type RichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type NativeRichTextEditorValueJSONUpdateMode, type NativeRichTextEditorAutoCapitalize, type NativeRichTextEditorKeyboardType, type NativeRichTextEditorAndroidInputOptions, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, } from './RichTextEditorTypes';
|
|
2
2
|
export { RichTextEditor, NativeRichTextEditor } from './RichTextEditor';
|
|
3
3
|
export type { NativeRichTextEditorFocusPreservingElement, NativeRichTextEditorFocusPreservingRef, NativeRichTextEditorFocusPreservingRefs, RichTextEditorFocusPreservingElement, RichTextEditorFocusPreservingRef, RichTextEditorFocusPreservingRefs, } from './useFocusPreservingFrames';
|
|
@@ -2,7 +2,7 @@ import { type ExternalTextCompositionManager, type NativeExternalTextComposition
|
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
3
|
import { type NativeSyntheticEvent, type StyleProp, type ViewStyle } from 'react-native';
|
|
4
4
|
import { type DocumentJSON, type NativeEditorDocumentHandle } from './NativeEditorBridge';
|
|
5
|
-
import { type RichTextEditorAutoCapitalize, type RichTextEditorKeyboardType, type RichTextEditorToolbarPlacement, type RichTextEditorHeightBehavior } from './RichTextEditorTypes';
|
|
5
|
+
import { type RichTextEditorPasteMode, type RichTextEditorAutoCapitalize, type RichTextEditorKeyboardType, type RichTextEditorToolbarPlacement, type RichTextEditorHeightBehavior } from './RichTextEditorTypes';
|
|
6
6
|
export interface NativeExternalTextCompositionEvent {
|
|
7
7
|
editorId: string;
|
|
8
8
|
resultJson: string;
|
|
@@ -21,6 +21,7 @@ export interface NativeEditorViewProps {
|
|
|
21
21
|
editorId: string;
|
|
22
22
|
placeholder?: string;
|
|
23
23
|
editable: boolean;
|
|
24
|
+
pasteMode: RichTextEditorPasteMode;
|
|
24
25
|
autoFocus: boolean;
|
|
25
26
|
autoCapitalize?: RichTextEditorAutoCapitalize;
|
|
26
27
|
autoCorrect?: boolean;
|
|
@@ -24,6 +24,8 @@ export type RichTextEditorToolbarPlacement = 'keyboard' | 'inline';
|
|
|
24
24
|
* records one undoable step, `'reset'` clears history entirely.
|
|
25
25
|
*/
|
|
26
26
|
export type RichTextEditorValueJSONUpdateMode = 'replace' | 'reset';
|
|
27
|
+
/** Controls native clipboard paste. Plain text keeps destination typing marks. */
|
|
28
|
+
export type RichTextEditorPasteMode = 'rich' | 'plainText' | 'disabled';
|
|
27
29
|
/** Native keyboard auto-capitalization behavior. */
|
|
28
30
|
export type RichTextEditorAutoCapitalize = 'none' | 'sentences' | 'words' | 'characters';
|
|
29
31
|
/** Native keyboard layout. Values not supported by a platform fall back to its default. */
|
|
@@ -104,6 +106,8 @@ export interface RichTextEditorProps {
|
|
|
104
106
|
placeholder?: string;
|
|
105
107
|
/** Whether the editor is editable. Defaults to true. When false, every mutation ref method rejects with MUTATION_REJECTED; selection and controlled content still flow. */
|
|
106
108
|
editable?: boolean;
|
|
109
|
+
/** Clipboard paste policy. Defaults to rich; updates apply immediately. */
|
|
110
|
+
pasteMode?: RichTextEditorPasteMode;
|
|
107
111
|
/** Whether to auto-focus on mount. */
|
|
108
112
|
autoFocus?: boolean;
|
|
109
113
|
/** Controls native keyboard auto-capitalization. Defaults to sentences. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { RichTextEditor, type RichTextEditorProps, type RichTextEditorRef, type RichTextEditorCaretRect, type RichTextEditorHeightBehavior, type RichTextEditorToolbarPlacement, type RichTextEditorValueJSONUpdateMode, type RichTextEditorAutoCapitalize, type RichTextEditorKeyboardType, type RichTextEditorAndroidInputOptions, type RichTextEditorFocusPreservingElement, type RichTextEditorFocusPreservingRef, type RichTextEditorFocusPreservingRefs, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type NativeRichTextEditorValueJSONUpdateMode, type NativeRichTextEditorAutoCapitalize, type NativeRichTextEditorKeyboardType, type NativeRichTextEditorAndroidInputOptions, type NativeRichTextEditorFocusPreservingElement, type NativeRichTextEditorFocusPreservingRef, type NativeRichTextEditorFocusPreservingRefs, } from './NativeRichTextEditor';
|
|
1
|
+
export { RichTextEditor, type RichTextEditorProps, type RichTextEditorRef, type RichTextEditorCaretRect, type RichTextEditorHeightBehavior, type RichTextEditorToolbarPlacement, type RichTextEditorValueJSONUpdateMode, type RichTextEditorPasteMode, type RichTextEditorAutoCapitalize, type RichTextEditorKeyboardType, type RichTextEditorAndroidInputOptions, type RichTextEditorFocusPreservingElement, type RichTextEditorFocusPreservingRef, type RichTextEditorFocusPreservingRefs, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type NativeRichTextEditorValueJSONUpdateMode, type NativeRichTextEditorAutoCapitalize, type NativeRichTextEditorKeyboardType, type NativeRichTextEditorAndroidInputOptions, type NativeRichTextEditorFocusPreservingElement, type NativeRichTextEditorFocusPreservingRef, type NativeRichTextEditorFocusPreservingRefs, } from './NativeRichTextEditor';
|
|
2
2
|
export type { ExternalTextCompositionEndCause, ExternalTextCompositionEndEvent, ExternalTextCompositionOptions, ExternalTextCompositionSession, } from './ExternalTextComposition';
|
|
3
3
|
export { RichTextViewer, type RichTextViewerProps, type RichTextViewerAddons, type RichTextViewerBaseProps, type RichTextViewerMentionsConfig, type RichTextViewerLinkPressEvent, type RichTextViewerMentionPressEvent, type RichTextViewerErrorEvent, type RichTextViewerAtomAttrsUpdateEvent, NativeProseViewer, type NativeProseViewerProps, type NativeProseViewerAddons, type NativeProseViewerBaseProps, type NativeProseViewerMentionsConfig, type NativeProseViewerLinkPressEvent, type NativeProseViewerMentionPressEvent, type NativeProseViewerErrorEvent, type NativeProseViewerAtomAttrsUpdateEvent, } from './NativeProseViewer';
|
|
4
4
|
export { EditorToolbar, DEFAULT_EDITOR_TOOLBAR_ITEMS, type EditorToolbarProps, type EditorToolbarItem, type EditorToolbarLeafItem, type EditorToolbarGroupChildItem, type EditorToolbarGroupItem, type EditorToolbarGroupPresentation, type EditorToolbarIcon, type EditorToolbarButtonStyle, type EditorToolbarDefaultIconId, type EditorToolbarSFSymbolIcon, type EditorToolbarMaterialIcon, type EditorToolbarCommand, type EditorToolbarHeadingLevel, type EditorToolbarListType, type EditorToolbarItemPlacement, } from './EditorToolbar';
|