@apollohg/react-native-prose-editor 0.5.14 → 0.5.16
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 +187 -4
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +12 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +9 -0
- package/dist/EditorToolbar.d.ts +12 -1
- package/dist/EditorToolbar.js +131 -2
- package/dist/NativeRichTextEditor.d.ts +8 -0
- package/dist/NativeRichTextEditor.js +112 -19
- package/dist/index.d.ts +1 -1
- 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 +14 -2
- package/ios/NativeEditorModule.swift +9 -0
- package/ios/RichTextEditorView.swift +172 -5
- 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
|
@@ -7,6 +7,7 @@ import android.graphics.Rect
|
|
|
7
7
|
import android.graphics.RectF
|
|
8
8
|
import android.text.Annotation
|
|
9
9
|
import android.text.Editable
|
|
10
|
+
import android.text.InputType
|
|
10
11
|
import android.text.Layout
|
|
11
12
|
import android.text.Spanned
|
|
12
13
|
import android.text.StaticLayout
|
|
@@ -17,8 +18,10 @@ import android.util.Log
|
|
|
17
18
|
import android.util.TypedValue
|
|
18
19
|
import android.view.KeyEvent
|
|
19
20
|
import android.view.MotionEvent
|
|
21
|
+
import android.view.inputmethod.BaseInputConnection
|
|
20
22
|
import android.view.inputmethod.EditorInfo
|
|
21
23
|
import android.view.inputmethod.InputConnection
|
|
24
|
+
import android.view.inputmethod.InputMethodManager
|
|
22
25
|
import androidx.appcompat.widget.AppCompatEditText
|
|
23
26
|
import kotlin.math.roundToInt
|
|
24
27
|
import uniffi.editor_core.* // UniFFI-generated bindings
|
|
@@ -105,6 +108,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
105
108
|
val end: Int
|
|
106
109
|
)
|
|
107
110
|
|
|
111
|
+
private data class NativeTextMutation(
|
|
112
|
+
val scalarFrom: Int,
|
|
113
|
+
val scalarTo: Int,
|
|
114
|
+
val replacementText: String,
|
|
115
|
+
val resultingText: String
|
|
116
|
+
)
|
|
117
|
+
|
|
108
118
|
/**
|
|
109
119
|
* Listener interface for editor events, parallel to iOS's EditorTextViewDelegate.
|
|
110
120
|
*/
|
|
@@ -162,6 +172,9 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
162
172
|
var heightBehavior: EditorHeightBehavior = EditorHeightBehavior.FIXED
|
|
163
173
|
private set
|
|
164
174
|
private var imageResizingEnabled = true
|
|
175
|
+
private var nativeAutoCapitalize = DEFAULT_AUTO_CAPITALIZE
|
|
176
|
+
private var nativeAutoCorrect = DEFAULT_AUTO_CORRECT
|
|
177
|
+
private var nativeKeyboardType = DEFAULT_KEYBOARD_TYPE
|
|
165
178
|
|
|
166
179
|
private var contentInsets: EditorContentInsets? = null
|
|
167
180
|
private var viewportBottomInsetPx: Int = 0
|
|
@@ -198,10 +211,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
198
211
|
|
|
199
212
|
init {
|
|
200
213
|
// Configure for rich text editing.
|
|
201
|
-
inputType =
|
|
202
|
-
EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE or
|
|
203
|
-
EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT or
|
|
204
|
-
EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
214
|
+
inputType = resolvedInputType()
|
|
205
215
|
|
|
206
216
|
// Disable built-in spell checking to avoid conflicts with Rust state.
|
|
207
217
|
// The Rust editor is the source of truth for text content.
|
|
@@ -224,6 +234,110 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
224
234
|
updateEffectivePadding()
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
fun setAutoCapitalize(autoCapitalize: String?) {
|
|
238
|
+
val next = when (autoCapitalize) {
|
|
239
|
+
"none",
|
|
240
|
+
"sentences",
|
|
241
|
+
"words",
|
|
242
|
+
"characters" -> autoCapitalize
|
|
243
|
+
else -> DEFAULT_AUTO_CAPITALIZE
|
|
244
|
+
}
|
|
245
|
+
if (nativeAutoCapitalize == next) return
|
|
246
|
+
nativeAutoCapitalize = next
|
|
247
|
+
applyInputTraits()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fun setAutoCorrect(autoCorrect: Boolean?) {
|
|
251
|
+
val next = autoCorrect ?: DEFAULT_AUTO_CORRECT
|
|
252
|
+
if (nativeAutoCorrect == next) return
|
|
253
|
+
nativeAutoCorrect = next
|
|
254
|
+
applyInputTraits()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fun setKeyboardType(keyboardType: String?) {
|
|
258
|
+
val next = when (keyboardType) {
|
|
259
|
+
"default",
|
|
260
|
+
"email-address",
|
|
261
|
+
"numeric",
|
|
262
|
+
"phone-pad",
|
|
263
|
+
"ascii-capable",
|
|
264
|
+
"numbers-and-punctuation",
|
|
265
|
+
"url",
|
|
266
|
+
"number-pad",
|
|
267
|
+
"name-phone-pad",
|
|
268
|
+
"decimal-pad",
|
|
269
|
+
"twitter",
|
|
270
|
+
"web-search",
|
|
271
|
+
"visible-password",
|
|
272
|
+
"ascii-capable-number-pad" -> keyboardType
|
|
273
|
+
else -> DEFAULT_KEYBOARD_TYPE
|
|
274
|
+
}
|
|
275
|
+
if (nativeKeyboardType == next) return
|
|
276
|
+
nativeKeyboardType = next
|
|
277
|
+
applyInputTraits()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private fun applyInputTraits() {
|
|
281
|
+
val nextInputType = resolvedInputType()
|
|
282
|
+
if (inputType == nextInputType) return
|
|
283
|
+
|
|
284
|
+
val currentStart = selectionStart
|
|
285
|
+
val currentEnd = selectionEnd
|
|
286
|
+
setRawInputType(nextInputType)
|
|
287
|
+
|
|
288
|
+
val editable = text
|
|
289
|
+
if (
|
|
290
|
+
editable != null &&
|
|
291
|
+
currentStart >= 0 &&
|
|
292
|
+
currentEnd >= 0 &&
|
|
293
|
+
currentStart <= editable.length &&
|
|
294
|
+
currentEnd <= editable.length
|
|
295
|
+
) {
|
|
296
|
+
setSelection(currentStart, currentEnd)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (hasFocus()) {
|
|
300
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
301
|
+
imm?.restartInput(this)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private fun resolvedInputType(): Int {
|
|
306
|
+
var nextInputType = when (nativeKeyboardType) {
|
|
307
|
+
"email-address" -> InputType.TYPE_CLASS_TEXT or
|
|
308
|
+
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
|
309
|
+
"url" -> InputType.TYPE_CLASS_TEXT or
|
|
310
|
+
InputType.TYPE_TEXT_VARIATION_URI
|
|
311
|
+
"phone-pad" -> InputType.TYPE_CLASS_PHONE
|
|
312
|
+
"number-pad" -> InputType.TYPE_CLASS_NUMBER
|
|
313
|
+
"decimal-pad" -> InputType.TYPE_CLASS_NUMBER or
|
|
314
|
+
InputType.TYPE_NUMBER_FLAG_DECIMAL
|
|
315
|
+
"numeric" -> InputType.TYPE_CLASS_NUMBER or
|
|
316
|
+
InputType.TYPE_NUMBER_FLAG_DECIMAL or
|
|
317
|
+
InputType.TYPE_NUMBER_FLAG_SIGNED
|
|
318
|
+
"visible-password" -> InputType.TYPE_CLASS_TEXT or
|
|
319
|
+
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
|
320
|
+
else -> InputType.TYPE_CLASS_TEXT
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if ((nextInputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
|
|
324
|
+
nextInputType = nextInputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
325
|
+
nextInputType = nextInputType or when (nativeAutoCapitalize) {
|
|
326
|
+
"none" -> 0
|
|
327
|
+
"words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
|
328
|
+
"characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
329
|
+
else -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
330
|
+
}
|
|
331
|
+
nextInputType = nextInputType or if (nativeAutoCorrect) {
|
|
332
|
+
InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
|
|
333
|
+
} else {
|
|
334
|
+
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return nextInputType
|
|
339
|
+
}
|
|
340
|
+
|
|
227
341
|
// ── InputConnection Override ────────────────────────────────────────
|
|
228
342
|
|
|
229
343
|
/**
|
|
@@ -1117,6 +1231,66 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1117
1231
|
applyUpdateJSON(updateJSON)
|
|
1118
1232
|
}
|
|
1119
1233
|
|
|
1234
|
+
private fun nativeTextMutationFromAuthorizedDiff(currentText: String): NativeTextMutation? {
|
|
1235
|
+
val authorizedText = lastAuthorizedText
|
|
1236
|
+
if (currentText == authorizedText) return null
|
|
1237
|
+
|
|
1238
|
+
var prefix = 0
|
|
1239
|
+
val sharedLength = minOf(authorizedText.length, currentText.length)
|
|
1240
|
+
while (
|
|
1241
|
+
prefix < sharedLength &&
|
|
1242
|
+
authorizedText[prefix] == currentText[prefix]
|
|
1243
|
+
) {
|
|
1244
|
+
prefix++
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
var authorizedEnd = authorizedText.length
|
|
1248
|
+
var currentEnd = currentText.length
|
|
1249
|
+
while (
|
|
1250
|
+
authorizedEnd > prefix &&
|
|
1251
|
+
currentEnd > prefix &&
|
|
1252
|
+
authorizedText[authorizedEnd - 1] == currentText[currentEnd - 1]
|
|
1253
|
+
) {
|
|
1254
|
+
authorizedEnd--
|
|
1255
|
+
currentEnd--
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
val replacementText = currentText.substring(prefix, currentEnd)
|
|
1259
|
+
return NativeTextMutation(
|
|
1260
|
+
scalarFrom = PositionBridge.utf16ToScalar(prefix, authorizedText),
|
|
1261
|
+
scalarTo = PositionBridge.utf16ToScalar(authorizedEnd, authorizedText),
|
|
1262
|
+
replacementText = replacementText,
|
|
1263
|
+
resultingText = currentText
|
|
1264
|
+
)
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
private fun shouldAdoptNativeTextMutation(editable: Editable?): Boolean {
|
|
1268
|
+
if (!isEditable || !hasFocus()) return false
|
|
1269
|
+
if (editable == null) return true
|
|
1270
|
+
|
|
1271
|
+
val composingStart = BaseInputConnection.getComposingSpanStart(editable)
|
|
1272
|
+
val composingEnd = BaseInputConnection.getComposingSpanEnd(editable)
|
|
1273
|
+
return composingStart < 0 || composingEnd < 0 || composingStart == composingEnd
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private fun commitNativeTextMutation(mutation: NativeTextMutation) {
|
|
1277
|
+
if ((text?.toString() ?: "") != mutation.resultingText) return
|
|
1278
|
+
|
|
1279
|
+
if (mutation.scalarFrom == mutation.scalarTo) {
|
|
1280
|
+
if (mutation.replacementText.isNotEmpty()) {
|
|
1281
|
+
insertTextInRust(mutation.replacementText, mutation.scalarFrom)
|
|
1282
|
+
}
|
|
1283
|
+
} else if (mutation.replacementText.isEmpty()) {
|
|
1284
|
+
deleteRangeInRust(mutation.scalarFrom, mutation.scalarTo)
|
|
1285
|
+
} else {
|
|
1286
|
+
replaceTextRangeInRust(
|
|
1287
|
+
mutation.scalarFrom,
|
|
1288
|
+
mutation.scalarTo,
|
|
1289
|
+
mutation.replacementText
|
|
1290
|
+
)
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1120
1294
|
/**
|
|
1121
1295
|
* Delete a scalar range via the Rust editor.
|
|
1122
1296
|
*
|
|
@@ -1912,6 +2086,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1912
2086
|
val currentText = s?.toString() ?: ""
|
|
1913
2087
|
if (currentText == lastAuthorizedText) return
|
|
1914
2088
|
|
|
2089
|
+
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
2090
|
+
if (mutation != null && shouldAdoptNativeTextMutation(s)) {
|
|
2091
|
+
commitNativeTextMutation(mutation)
|
|
2092
|
+
return
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1915
2095
|
// Text has diverged from Rust's authorized state.
|
|
1916
2096
|
reconciliationCount++
|
|
1917
2097
|
Log.w(
|
|
@@ -1929,6 +2109,9 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1929
2109
|
}
|
|
1930
2110
|
|
|
1931
2111
|
companion object {
|
|
2112
|
+
private const val DEFAULT_AUTO_CAPITALIZE = "sentences"
|
|
2113
|
+
private const val DEFAULT_AUTO_CORRECT = true
|
|
2114
|
+
private const val DEFAULT_KEYBOARD_TYPE = "default"
|
|
1932
2115
|
private const val EMPTY_BLOCK_PLACEHOLDER = '\u200B'
|
|
1933
2116
|
private const val LOG_TAG = "NativeEditor"
|
|
1934
2117
|
}
|
|
@@ -181,6 +181,18 @@ class NativeEditorExpoView(
|
|
|
181
181
|
focus()
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
fun setAutoCapitalize(autoCapitalize: String?) {
|
|
185
|
+
richTextView.editorEditText.setAutoCapitalize(autoCapitalize)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fun setAutoCorrect(autoCorrect: Boolean?) {
|
|
189
|
+
richTextView.editorEditText.setAutoCorrect(autoCorrect)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fun setKeyboardType(keyboardType: String?) {
|
|
193
|
+
richTextView.editorEditText.setKeyboardType(keyboardType)
|
|
194
|
+
}
|
|
195
|
+
|
|
184
196
|
fun setShowToolbar(showToolbar: Boolean) {
|
|
185
197
|
showsToolbar = showToolbar
|
|
186
198
|
updateKeyboardToolbarVisibility()
|
|
@@ -336,6 +336,15 @@ class NativeEditorModule : Module() {
|
|
|
336
336
|
Prop("autoFocus") { view: NativeEditorExpoView, autoFocus: Boolean ->
|
|
337
337
|
view.setAutoFocus(autoFocus)
|
|
338
338
|
}
|
|
339
|
+
Prop("autoCapitalize") { view: NativeEditorExpoView, autoCapitalize: String? ->
|
|
340
|
+
view.setAutoCapitalize(autoCapitalize)
|
|
341
|
+
}
|
|
342
|
+
Prop("autoCorrect") { view: NativeEditorExpoView, autoCorrect: Boolean? ->
|
|
343
|
+
view.setAutoCorrect(autoCorrect)
|
|
344
|
+
}
|
|
345
|
+
Prop("keyboardType") { view: NativeEditorExpoView, keyboardType: String? ->
|
|
346
|
+
view.setKeyboardType(keyboardType)
|
|
347
|
+
}
|
|
339
348
|
Prop("showToolbar") { view: NativeEditorExpoView, showToolbar: Boolean ->
|
|
340
349
|
view.setShowToolbar(showToolbar)
|
|
341
350
|
}
|
package/dist/EditorToolbar.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ActiveState, HistoryState } from './NativeEditorBridge';
|
|
2
|
-
import type { EditorToolbarTheme } from './EditorTheme';
|
|
2
|
+
import type { EditorMentionTheme, EditorToolbarTheme } from './EditorTheme';
|
|
3
|
+
import type { MentionSuggestion } from './addons';
|
|
3
4
|
export type EditorToolbarListType = 'bulletList' | 'orderedList';
|
|
4
5
|
export type EditorToolbarHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
5
6
|
export type EditorToolbarCommand = 'indentList' | 'outdentList' | 'undo' | 'redo';
|
|
@@ -97,8 +98,17 @@ export interface EditorToolbarFrame {
|
|
|
97
98
|
width: number;
|
|
98
99
|
height: number;
|
|
99
100
|
}
|
|
101
|
+
interface EditorToolbarMentionState {
|
|
102
|
+
ownerId: number;
|
|
103
|
+
trigger: string;
|
|
104
|
+
suggestions: readonly MentionSuggestion[];
|
|
105
|
+
theme?: EditorMentionTheme;
|
|
106
|
+
suggestionThemes?: Readonly<Record<string, EditorMentionTheme | undefined>>;
|
|
107
|
+
onSelectSuggestion: (suggestion: MentionSuggestion) => void;
|
|
108
|
+
}
|
|
100
109
|
export declare function isEditorToolbarFocusPreservationActive(): boolean;
|
|
101
110
|
export declare function useEditorToolbarFrames(): readonly EditorToolbarFrame[];
|
|
111
|
+
export declare function setEditorToolbarMentionState(ownerId: number, state: Omit<EditorToolbarMentionState, 'ownerId'> | null): void;
|
|
102
112
|
export declare function _setEditorToolbarFrameForTests(id: number, frame: EditorToolbarFrame | null): void;
|
|
103
113
|
export declare function _resetEditorToolbarFrameRegistryForTests(): void;
|
|
104
114
|
export declare function _beginEditorToolbarInteractionForTests(): void;
|
|
@@ -164,3 +174,4 @@ export interface EditorToolbarProps {
|
|
|
164
174
|
preserveEditorFocus?: boolean;
|
|
165
175
|
}
|
|
166
176
|
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;
|
|
177
|
+
export {};
|
package/dist/EditorToolbar.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = void 0;
|
|
4
4
|
exports.isEditorToolbarFocusPreservationActive = isEditorToolbarFocusPreservationActive;
|
|
5
5
|
exports.useEditorToolbarFrames = useEditorToolbarFrames;
|
|
6
|
+
exports.setEditorToolbarMentionState = setEditorToolbarMentionState;
|
|
6
7
|
exports._setEditorToolbarFrameForTests = _setEditorToolbarFrameForTests;
|
|
7
8
|
exports._resetEditorToolbarFrameRegistryForTests = _resetEditorToolbarFrameRegistryForTests;
|
|
8
9
|
exports._beginEditorToolbarInteractionForTests = _beginEditorToolbarInteractionForTests;
|
|
@@ -14,9 +15,11 @@ const react_1 = require("react");
|
|
|
14
15
|
const react_native_1 = require("react-native");
|
|
15
16
|
const editorToolbarFrames = new Map();
|
|
16
17
|
const editorToolbarFrameListeners = new Set();
|
|
18
|
+
const editorToolbarMentionStateListeners = new Set();
|
|
17
19
|
let nextEditorToolbarRegistrationId = 1;
|
|
18
20
|
let activeEditorToolbarInteractions = 0;
|
|
19
21
|
let editorToolbarFocusPreserveUntil = 0;
|
|
22
|
+
let editorToolbarMentionState = null;
|
|
20
23
|
const EDITOR_TOOLBAR_FOCUS_PRESERVE_MS = 750;
|
|
21
24
|
function areToolbarFramesEqual(left, right) {
|
|
22
25
|
return (left?.x === right?.x &&
|
|
@@ -27,9 +30,24 @@ function areToolbarFramesEqual(left, right) {
|
|
|
27
30
|
function notifyEditorToolbarFrameListeners() {
|
|
28
31
|
editorToolbarFrameListeners.forEach((listener) => listener());
|
|
29
32
|
}
|
|
33
|
+
function notifyEditorToolbarMentionStateListeners() {
|
|
34
|
+
editorToolbarMentionStateListeners.forEach((listener) => listener());
|
|
35
|
+
}
|
|
30
36
|
function getEditorToolbarFramesSnapshot() {
|
|
31
37
|
return Array.from(editorToolbarFrames.values());
|
|
32
38
|
}
|
|
39
|
+
function subscribeEditorToolbarMentionState(listener) {
|
|
40
|
+
editorToolbarMentionStateListeners.add(listener);
|
|
41
|
+
return () => {
|
|
42
|
+
editorToolbarMentionStateListeners.delete(listener);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function getEditorToolbarMentionStateSnapshot() {
|
|
46
|
+
return editorToolbarMentionState;
|
|
47
|
+
}
|
|
48
|
+
function useEditorToolbarMentionState() {
|
|
49
|
+
return (0, react_1.useSyncExternalStore)(subscribeEditorToolbarMentionState, getEditorToolbarMentionStateSnapshot, getEditorToolbarMentionStateSnapshot);
|
|
50
|
+
}
|
|
33
51
|
function registerEditorToolbarFrame(id, frame) {
|
|
34
52
|
if (frame == null || frame.width <= 0 || frame.height <= 0) {
|
|
35
53
|
if (editorToolbarFrames.delete(id)) {
|
|
@@ -75,14 +93,31 @@ function useEditorToolbarFrames() {
|
|
|
75
93
|
}, []);
|
|
76
94
|
return frames;
|
|
77
95
|
}
|
|
96
|
+
function setEditorToolbarMentionState(ownerId, state) {
|
|
97
|
+
if (state == null) {
|
|
98
|
+
if (editorToolbarMentionState?.ownerId !== ownerId) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
editorToolbarMentionState = null;
|
|
102
|
+
notifyEditorToolbarMentionStateListeners();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
editorToolbarMentionState = {
|
|
106
|
+
ownerId,
|
|
107
|
+
...state,
|
|
108
|
+
};
|
|
109
|
+
notifyEditorToolbarMentionStateListeners();
|
|
110
|
+
}
|
|
78
111
|
function _setEditorToolbarFrameForTests(id, frame) {
|
|
79
112
|
registerEditorToolbarFrame(id, frame);
|
|
80
113
|
}
|
|
81
114
|
function _resetEditorToolbarFrameRegistryForTests() {
|
|
82
115
|
editorToolbarFrames.clear();
|
|
116
|
+
editorToolbarMentionState = null;
|
|
83
117
|
activeEditorToolbarInteractions = 0;
|
|
84
118
|
editorToolbarFocusPreserveUntil = 0;
|
|
85
119
|
notifyEditorToolbarFrameListeners();
|
|
120
|
+
notifyEditorToolbarMentionStateListeners();
|
|
86
121
|
}
|
|
87
122
|
function _beginEditorToolbarInteractionForTests() {
|
|
88
123
|
beginEditorToolbarInteraction();
|
|
@@ -194,6 +229,9 @@ const DEFAULT_MATERIAL_ICONS = {
|
|
|
194
229
|
undo: 'undo',
|
|
195
230
|
redo: 'redo',
|
|
196
231
|
};
|
|
232
|
+
function resolveMentionSuggestionLabel(suggestion, trigger) {
|
|
233
|
+
return suggestion.label?.trim() || `${trigger}${suggestion.title}`;
|
|
234
|
+
}
|
|
197
235
|
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, }) {
|
|
198
236
|
const marks = activeState.marks ?? {};
|
|
199
237
|
const nodes = activeState.nodes ?? {};
|
|
@@ -205,6 +243,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
205
243
|
const { width: windowWidth, height: windowHeight } = (0, react_native_1.useWindowDimensions)();
|
|
206
244
|
const [expandedGroupKey, setExpandedGroupKey] = (0, react_1.useState)(null);
|
|
207
245
|
const [menuState, setMenuState] = (0, react_1.useState)(null);
|
|
246
|
+
const mentionState = useEditorToolbarMentionState();
|
|
208
247
|
const toolbarInteractionActiveRef = (0, react_1.useRef)(false);
|
|
209
248
|
const framePublishAnimationFramesRef = (0, react_1.useRef)([]);
|
|
210
249
|
const framePublishTimeoutsRef = (0, react_1.useRef)([]);
|
|
@@ -216,6 +255,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
216
255
|
const isInList = !!nodes['bulletList'] || !!nodes['orderedList'];
|
|
217
256
|
const canIndentList = isInList && !!commands['indentList'];
|
|
218
257
|
const canOutdentList = isInList && !!commands['outdentList'];
|
|
258
|
+
const shouldRenderMentionSuggestions = preserveEditorFocus && mentionState != null && mentionState.suggestions.length > 0;
|
|
219
259
|
const getActionForItem = (0, react_1.useCallback)((item) => {
|
|
220
260
|
switch (item.type) {
|
|
221
261
|
case 'mark':
|
|
@@ -558,6 +598,12 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
558
598
|
setMenuState(null);
|
|
559
599
|
}
|
|
560
600
|
}, [groupsByKey, menuState]);
|
|
601
|
+
(0, react_1.useEffect)(() => {
|
|
602
|
+
if (shouldRenderMentionSuggestions) {
|
|
603
|
+
setExpandedGroupKey(null);
|
|
604
|
+
setMenuState(null);
|
|
605
|
+
}
|
|
606
|
+
}, [shouldRenderMentionSuggestions]);
|
|
561
607
|
const handleButtonPress = (0, react_1.useCallback)((button) => {
|
|
562
608
|
button.action();
|
|
563
609
|
if (button.groupKey) {
|
|
@@ -665,7 +711,65 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
665
711
|
{
|
|
666
712
|
borderRadius: theme?.borderRadius ?? TOOLBAR_RADIUS,
|
|
667
713
|
},
|
|
668
|
-
], children: [(0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false,
|
|
714
|
+
], children: [shouldRenderMentionSuggestions && mentionState != null ? ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { testID: 'editor-toolbar-mention-suggestions', horizontal: true, showsHorizontalScrollIndicator: false, style: [
|
|
715
|
+
styles.mentionSuggestionsScroll,
|
|
716
|
+
{
|
|
717
|
+
backgroundColor: mentionState.theme?.popoverBackgroundColor ??
|
|
718
|
+
mentionState.theme?.backgroundColor ??
|
|
719
|
+
'transparent',
|
|
720
|
+
borderColor: mentionState.theme?.popoverBorderColor ??
|
|
721
|
+
mentionState.theme?.borderColor ??
|
|
722
|
+
'transparent',
|
|
723
|
+
borderWidth: mentionState.theme?.popoverBorderWidth ??
|
|
724
|
+
mentionState.theme?.borderWidth ??
|
|
725
|
+
0,
|
|
726
|
+
borderRadius: mentionState.theme?.popoverBorderRadius ??
|
|
727
|
+
mentionState.theme?.borderRadius ??
|
|
728
|
+
0,
|
|
729
|
+
},
|
|
730
|
+
mentionState.theme?.popoverShadowColor != null
|
|
731
|
+
? {
|
|
732
|
+
shadowColor: mentionState.theme.popoverShadowColor,
|
|
733
|
+
shadowOpacity: 0.14,
|
|
734
|
+
shadowRadius: 12,
|
|
735
|
+
shadowOffset: { width: 0, height: 4 },
|
|
736
|
+
elevation: 8,
|
|
737
|
+
}
|
|
738
|
+
: null,
|
|
739
|
+
], contentContainerStyle: styles.mentionSuggestionsContent, keyboardShouldPersistTaps: 'always', children: mentionState.suggestions.map((suggestion) => {
|
|
740
|
+
const label = resolveMentionSuggestionLabel(suggestion, mentionState.trigger);
|
|
741
|
+
const suggestionTheme = mentionState.suggestionThemes?.[suggestion.key] ?? mentionState.theme;
|
|
742
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `editor-toolbar-mention-suggestion-${suggestion.key}`, accessibilityRole: 'button', accessibilityLabel: label, onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: () => mentionState.onSelectSuggestion(suggestion), style: ({ pressed }) => [
|
|
743
|
+
styles.mentionSuggestion,
|
|
744
|
+
{
|
|
745
|
+
backgroundColor: pressed
|
|
746
|
+
? (suggestionTheme?.optionHighlightedBackgroundColor ??
|
|
747
|
+
'rgba(0, 122, 255, 0.12)')
|
|
748
|
+
: (suggestionTheme?.backgroundColor ?? '#F2F2F7'),
|
|
749
|
+
borderColor: suggestionTheme?.borderColor ?? 'transparent',
|
|
750
|
+
borderWidth: suggestionTheme?.borderWidth ?? 0,
|
|
751
|
+
borderRadius: suggestionTheme?.borderRadius ?? 12,
|
|
752
|
+
},
|
|
753
|
+
], children: ({ pressed }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
|
|
754
|
+
styles.mentionSuggestionTitle,
|
|
755
|
+
{
|
|
756
|
+
fontWeight: suggestionTheme?.fontWeight ?? '600',
|
|
757
|
+
color: pressed
|
|
758
|
+
? (suggestionTheme?.optionHighlightedTextColor ??
|
|
759
|
+
suggestionTheme?.optionTextColor ??
|
|
760
|
+
'#000000')
|
|
761
|
+
: (suggestionTheme?.optionTextColor ??
|
|
762
|
+
suggestionTheme?.textColor ??
|
|
763
|
+
'#000000'),
|
|
764
|
+
},
|
|
765
|
+
], children: label }), suggestion.subtitle ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
|
|
766
|
+
styles.mentionSuggestionSubtitle,
|
|
767
|
+
{
|
|
768
|
+
color: suggestionTheme?.optionSecondaryTextColor ??
|
|
769
|
+
'#8E8E93',
|
|
770
|
+
},
|
|
771
|
+
], children: suggestion.subtitle })) : null] })) }, suggestion.key));
|
|
772
|
+
}) })) : ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.scrollContent, keyboardShouldPersistTaps: 'always', onScrollBeginDrag: () => setMenuState(null), children: renderedItems.map((item) => {
|
|
669
773
|
if (item.type === 'separator') {
|
|
670
774
|
return renderSeparator(item.key);
|
|
671
775
|
}
|
|
@@ -683,7 +787,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
683
787
|
});
|
|
684
788
|
}
|
|
685
789
|
return renderButton(item.button, () => handleButtonPress(item.button));
|
|
686
|
-
}) }), menuState != null && menuGroup != null ? ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { transparent: true, visible: true, animationType: 'fade', onRequestClose: () => setMenuState(null), children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.menuBackdrop, onPress: () => setMenuState(null), children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
790
|
+
}) })), !shouldRenderMentionSuggestions && menuState != null && menuGroup != null ? ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { transparent: true, visible: true, animationType: 'fade', onRequestClose: () => setMenuState(null), children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.menuBackdrop, onPress: () => setMenuState(null), children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
687
791
|
styles.menuCard,
|
|
688
792
|
{
|
|
689
793
|
top: menuTop,
|
|
@@ -760,6 +864,31 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
760
864
|
paddingHorizontal: TOOLBAR_PADDING_H,
|
|
761
865
|
minWidth: '100%',
|
|
762
866
|
},
|
|
867
|
+
mentionSuggestionsContent: {
|
|
868
|
+
paddingHorizontal: 12,
|
|
869
|
+
paddingVertical: 4,
|
|
870
|
+
alignItems: 'center',
|
|
871
|
+
minWidth: '100%',
|
|
872
|
+
},
|
|
873
|
+
mentionSuggestionsScroll: {
|
|
874
|
+
overflow: 'hidden',
|
|
875
|
+
},
|
|
876
|
+
mentionSuggestion: {
|
|
877
|
+
minWidth: 88,
|
|
878
|
+
minHeight: 40,
|
|
879
|
+
marginRight: 8,
|
|
880
|
+
paddingHorizontal: 12,
|
|
881
|
+
paddingVertical: 8,
|
|
882
|
+
justifyContent: 'center',
|
|
883
|
+
},
|
|
884
|
+
mentionSuggestionTitle: {
|
|
885
|
+
fontSize: 14,
|
|
886
|
+
fontWeight: '600',
|
|
887
|
+
},
|
|
888
|
+
mentionSuggestionSubtitle: {
|
|
889
|
+
marginTop: 1,
|
|
890
|
+
fontSize: 12,
|
|
891
|
+
},
|
|
763
892
|
buttonAnchor: {
|
|
764
893
|
position: 'relative',
|
|
765
894
|
},
|
|
@@ -7,6 +7,8 @@ import { type EditorAddons } from './addons';
|
|
|
7
7
|
import { type ImageNodeAttributes, type SchemaDefinition } from './schemas';
|
|
8
8
|
export type NativeRichTextEditorHeightBehavior = 'fixed' | 'autoGrow';
|
|
9
9
|
export type NativeRichTextEditorToolbarPlacement = 'keyboard' | 'inline';
|
|
10
|
+
export type NativeRichTextEditorAutoCapitalize = 'none' | 'sentences' | 'words' | 'characters';
|
|
11
|
+
export type NativeRichTextEditorKeyboardType = 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'ascii-capable' | 'numbers-and-punctuation' | 'url' | 'number-pad' | 'name-phone-pad' | 'decimal-pad' | 'twitter' | 'web-search' | 'visible-password' | 'ascii-capable-number-pad';
|
|
10
12
|
export interface RemoteSelectionDecoration {
|
|
11
13
|
clientId: number;
|
|
12
14
|
anchor: number;
|
|
@@ -49,6 +51,12 @@ export interface NativeRichTextEditorProps {
|
|
|
49
51
|
maxLength?: number;
|
|
50
52
|
/** Whether to auto-focus on mount. */
|
|
51
53
|
autoFocus?: boolean;
|
|
54
|
+
/** Controls native keyboard auto-capitalization. Defaults to sentences. */
|
|
55
|
+
autoCapitalize?: NativeRichTextEditorAutoCapitalize;
|
|
56
|
+
/** Controls native keyboard autocorrection. Defaults to the platform-specific editor default. */
|
|
57
|
+
autoCorrect?: boolean;
|
|
58
|
+
/** Controls the native keyboard layout. Defaults to the platform default keyboard. */
|
|
59
|
+
keyboardType?: NativeRichTextEditorKeyboardType;
|
|
52
60
|
/** Controls whether the editor scrolls internally or grows with content. */
|
|
53
61
|
heightBehavior?: NativeRichTextEditorHeightBehavior;
|
|
54
62
|
/** Whether to show the formatting toolbar. Defaults to true. */
|
|
@@ -98,6 +98,19 @@ function resolveMentionSuggestionAttrs(suggestion, trigger) {
|
|
|
98
98
|
}
|
|
99
99
|
return attrs;
|
|
100
100
|
}
|
|
101
|
+
function mergeMentionSuggestionTheme(baseTheme, resolvedTheme) {
|
|
102
|
+
if (baseTheme == null && resolvedTheme == null) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const merged = {
|
|
106
|
+
...(baseTheme ?? {}),
|
|
107
|
+
...(resolvedTheme ?? {}),
|
|
108
|
+
};
|
|
109
|
+
if (resolvedTheme?.textColor != null && resolvedTheme.optionTextColor == null) {
|
|
110
|
+
merged.optionTextColor = resolvedTheme.textColor;
|
|
111
|
+
}
|
|
112
|
+
return merged;
|
|
113
|
+
}
|
|
101
114
|
const AUTO_LINK_URL_REGEX = /(?:https?:\/\/|www\.)\S+/giu;
|
|
102
115
|
const AUTO_LINK_INLINE_PLACEHOLDER = '\uFFFC';
|
|
103
116
|
const AUTO_LINK_LEADING_BOUNDARY_CHARS = new Set(['(', '[', '{', '<', '"', "'"]);
|
|
@@ -495,7 +508,7 @@ function useSerializedValue(value, serialize, revision) {
|
|
|
495
508
|
};
|
|
496
509
|
return serialized;
|
|
497
510
|
}
|
|
498
|
-
exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, schema, placeholder, editable = true, maxLength, autoFocus = false, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, autoDetectLinks = false, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
|
|
511
|
+
exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, schema, placeholder, editable = true, maxLength, autoFocus = false, autoCapitalize, autoCorrect, keyboardType, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, autoDetectLinks = false, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
|
|
499
512
|
const bridgeRef = (0, react_1.useRef)(null);
|
|
500
513
|
const nativeViewRef = (0, react_1.useRef)(null);
|
|
501
514
|
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
@@ -1215,6 +1228,88 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1215
1228
|
return bridgeRef.current.canRedo();
|
|
1216
1229
|
},
|
|
1217
1230
|
}), [insertImage, runAndApply]);
|
|
1231
|
+
const activeMentionTrigger = mentionQueryEvent?.trigger || resolveMentionTrigger(addons);
|
|
1232
|
+
const activeMentionSuggestions = (0, react_1.useMemo)(() => isFocused && mentionQueryEvent != null && addons?.mentions != null
|
|
1233
|
+
? filterMentionSuggestions(addons.mentions.suggestions ?? [], mentionQueryEvent.query, activeMentionTrigger)
|
|
1234
|
+
: [], [activeMentionTrigger, addons?.mentions, isFocused, mentionQueryEvent]);
|
|
1235
|
+
const inlineToolbarMentionTheme = theme?.mentions ?? addons?.mentions?.theme;
|
|
1236
|
+
const activeMentionSuggestionThemes = (0, react_1.useMemo)(() => {
|
|
1237
|
+
if (mentionQueryEvent == null ||
|
|
1238
|
+
addons?.mentions == null ||
|
|
1239
|
+
typeof addons.mentions.resolveTheme !== 'function' ||
|
|
1240
|
+
activeMentionSuggestions.length === 0) {
|
|
1241
|
+
return undefined;
|
|
1242
|
+
}
|
|
1243
|
+
const suggestionThemes = {};
|
|
1244
|
+
for (const suggestion of activeMentionSuggestions) {
|
|
1245
|
+
const selectionEvent = {
|
|
1246
|
+
trigger: activeMentionTrigger,
|
|
1247
|
+
suggestion,
|
|
1248
|
+
attrs: resolveMentionSuggestionAttrs(suggestion, activeMentionTrigger),
|
|
1249
|
+
range: mentionQueryEvent.range,
|
|
1250
|
+
};
|
|
1251
|
+
const attrs = resolveMentionSelectionAttrs(selectionEvent);
|
|
1252
|
+
let resolvedTheme;
|
|
1253
|
+
try {
|
|
1254
|
+
const nextTheme = addons.mentions.resolveTheme({
|
|
1255
|
+
...selectionEvent,
|
|
1256
|
+
attrs,
|
|
1257
|
+
});
|
|
1258
|
+
resolvedTheme = isRecord(nextTheme)
|
|
1259
|
+
? nextTheme
|
|
1260
|
+
: undefined;
|
|
1261
|
+
}
|
|
1262
|
+
catch (error) {
|
|
1263
|
+
if (__DEV__) {
|
|
1264
|
+
console.error('NativeRichTextEditor: mentions.resolveTheme threw', error);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const mergedTheme = mergeMentionSuggestionTheme(inlineToolbarMentionTheme, resolvedTheme);
|
|
1268
|
+
if (mergedTheme != null) {
|
|
1269
|
+
suggestionThemes[suggestion.key] = mergedTheme;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return Object.keys(suggestionThemes).length > 0 ? suggestionThemes : undefined;
|
|
1273
|
+
}, [
|
|
1274
|
+
activeMentionSuggestions,
|
|
1275
|
+
activeMentionTrigger,
|
|
1276
|
+
addons?.mentions,
|
|
1277
|
+
inlineToolbarMentionTheme,
|
|
1278
|
+
mentionQueryEvent,
|
|
1279
|
+
resolveMentionSelectionAttrs,
|
|
1280
|
+
]);
|
|
1281
|
+
const shouldPublishStandaloneMentionSuggestions = editable &&
|
|
1282
|
+
!showToolbar &&
|
|
1283
|
+
registeredToolbarFrames.length > 0 &&
|
|
1284
|
+
mentionQueryEvent != null &&
|
|
1285
|
+
activeMentionSuggestions.length > 0 &&
|
|
1286
|
+
addons?.mentions != null;
|
|
1287
|
+
(0, react_1.useEffect)(() => {
|
|
1288
|
+
if (editorInstanceId === 0) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (!shouldPublishStandaloneMentionSuggestions || mentionQueryEvent == null) {
|
|
1292
|
+
(0, EditorToolbar_1.setEditorToolbarMentionState)(editorInstanceId, null);
|
|
1293
|
+
return () => (0, EditorToolbar_1.setEditorToolbarMentionState)(editorInstanceId, null);
|
|
1294
|
+
}
|
|
1295
|
+
(0, EditorToolbar_1.setEditorToolbarMentionState)(editorInstanceId, {
|
|
1296
|
+
trigger: activeMentionTrigger,
|
|
1297
|
+
suggestions: activeMentionSuggestions,
|
|
1298
|
+
theme: inlineToolbarMentionTheme,
|
|
1299
|
+
suggestionThemes: activeMentionSuggestionThemes,
|
|
1300
|
+
onSelectSuggestion: handleInlineMentionSuggestionPress,
|
|
1301
|
+
});
|
|
1302
|
+
return () => (0, EditorToolbar_1.setEditorToolbarMentionState)(editorInstanceId, null);
|
|
1303
|
+
}, [
|
|
1304
|
+
activeMentionSuggestions,
|
|
1305
|
+
activeMentionSuggestionThemes,
|
|
1306
|
+
activeMentionTrigger,
|
|
1307
|
+
editorInstanceId,
|
|
1308
|
+
handleInlineMentionSuggestionPress,
|
|
1309
|
+
inlineToolbarMentionTheme,
|
|
1310
|
+
mentionQueryEvent,
|
|
1311
|
+
shouldPublishStandaloneMentionSuggestions,
|
|
1312
|
+
]);
|
|
1218
1313
|
if (!isReady)
|
|
1219
1314
|
return null;
|
|
1220
1315
|
const isLinkActive = activeState.marks.link === true;
|
|
@@ -1258,19 +1353,13 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1258
1353
|
};
|
|
1259
1354
|
const inlineToolbarMarginTop = theme?.toolbar?.marginTop ?? 8;
|
|
1260
1355
|
const inlineToolbarShowTopBorder = theme?.toolbar?.showTopBorder ?? false;
|
|
1261
|
-
const inlineToolbarMentionTheme = theme?.mentions ?? addons?.mentions?.theme;
|
|
1262
1356
|
const inlineToolbarContentTopBorderStyle = inlineToolbarShowTopBorder
|
|
1263
1357
|
? {
|
|
1264
1358
|
borderTopWidth: theme?.toolbar?.borderWidth ?? react_native_1.StyleSheet.hairlineWidth,
|
|
1265
1359
|
borderTopColor: theme?.toolbar?.borderColor ?? INLINE_TOOLBAR_BORDER_COLOR,
|
|
1266
1360
|
}
|
|
1267
1361
|
: null;
|
|
1268
|
-
const inlineMentionSuggestions = toolbarPlacement === 'inline'
|
|
1269
|
-
isFocused &&
|
|
1270
|
-
mentionQueryEvent != null &&
|
|
1271
|
-
addons?.mentions != null
|
|
1272
|
-
? filterMentionSuggestions(addons.mentions.suggestions ?? [], mentionQueryEvent.query, mentionQueryEvent.trigger || resolveMentionTrigger(addons))
|
|
1273
|
-
: [];
|
|
1362
|
+
const inlineMentionSuggestions = toolbarPlacement === 'inline' ? activeMentionSuggestions : [];
|
|
1274
1363
|
const shouldShowInlineMentionSuggestions = shouldRenderJsToolbar &&
|
|
1275
1364
|
toolbarPlacement === 'inline' &&
|
|
1276
1365
|
isFocused &&
|
|
@@ -1315,34 +1404,38 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1315
1404
|
inlineToolbarContentTopBorderStyle,
|
|
1316
1405
|
], children: (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.inlineMentionSuggestionsContent, keyboardShouldPersistTaps: 'always', children: inlineMentionSuggestions.map((suggestion) => {
|
|
1317
1406
|
const label = resolveMentionSuggestionLabel(suggestion, mentionQueryEvent?.trigger ?? resolveMentionTrigger(addons));
|
|
1407
|
+
const suggestionTheme = activeMentionSuggestionThemes?.[suggestion.key] ??
|
|
1408
|
+
inlineToolbarMentionTheme;
|
|
1318
1409
|
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `native-editor-inline-mention-suggestion-${suggestion.key}`, onPress: () => handleInlineMentionSuggestionPress(suggestion), accessibilityRole: 'button', accessibilityLabel: label, style: ({ pressed }) => [
|
|
1319
1410
|
styles.inlineMentionSuggestion,
|
|
1320
1411
|
{
|
|
1321
1412
|
backgroundColor: pressed
|
|
1322
|
-
? (
|
|
1413
|
+
? (suggestionTheme?.optionHighlightedBackgroundColor ??
|
|
1323
1414
|
'rgba(0, 122, 255, 0.12)')
|
|
1324
|
-
: (
|
|
1415
|
+
: (suggestionTheme?.backgroundColor ??
|
|
1325
1416
|
'#F2F2F7'),
|
|
1326
|
-
borderColor:
|
|
1417
|
+
borderColor: suggestionTheme?.borderColor ??
|
|
1327
1418
|
'transparent',
|
|
1328
|
-
borderWidth:
|
|
1329
|
-
borderRadius:
|
|
1419
|
+
borderWidth: suggestionTheme?.borderWidth ?? 0,
|
|
1420
|
+
borderRadius: suggestionTheme?.borderRadius ?? 12,
|
|
1330
1421
|
},
|
|
1331
1422
|
], children: ({ pressed }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
|
|
1332
1423
|
styles.inlineMentionSuggestionTitle,
|
|
1333
1424
|
{
|
|
1425
|
+
fontWeight: suggestionTheme?.fontWeight ??
|
|
1426
|
+
'600',
|
|
1334
1427
|
color: pressed
|
|
1335
|
-
? (
|
|
1336
|
-
|
|
1428
|
+
? (suggestionTheme?.optionHighlightedTextColor ??
|
|
1429
|
+
suggestionTheme?.optionTextColor ??
|
|
1337
1430
|
'#000000')
|
|
1338
|
-
: (
|
|
1339
|
-
|
|
1431
|
+
: (suggestionTheme?.optionTextColor ??
|
|
1432
|
+
suggestionTheme?.textColor ??
|
|
1340
1433
|
'#000000'),
|
|
1341
1434
|
},
|
|
1342
1435
|
], children: label }), suggestion.subtitle ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
|
|
1343
1436
|
styles.inlineMentionSuggestionSubtitle,
|
|
1344
1437
|
{
|
|
1345
|
-
color:
|
|
1438
|
+
color: suggestionTheme?.optionSecondaryTextColor ??
|
|
1346
1439
|
'#8E8E93',
|
|
1347
1440
|
},
|
|
1348
1441
|
], children: suggestion.subtitle })) : null] })) }, suggestion.key));
|
|
@@ -1372,7 +1465,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1372
1465
|
}), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
|
|
1373
1466
|
skipNativeApplyIfContentUnchanged: true,
|
|
1374
1467
|
}), 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) })) }));
|
|
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] }));
|
|
1468
|
+
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, autoCapitalize: autoCapitalize, autoCorrect: autoCorrect, keyboardType: keyboardType, 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] }));
|
|
1376
1469
|
});
|
|
1377
1470
|
const styles = react_native_1.StyleSheet.create({
|
|
1378
1471
|
container: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, 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 NativeRichTextEditorAutoCapitalize, type NativeRichTextEditorKeyboardType, 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';
|
|
Binary file
|
|
Binary file
|
|
@@ -1802,6 +1802,18 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1802
1802
|
focus()
|
|
1803
1803
|
}
|
|
1804
1804
|
|
|
1805
|
+
func setAutoCapitalize(_ autoCapitalize: String?) {
|
|
1806
|
+
richTextView.textView.setAutoCapitalize(autoCapitalize)
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
func setAutoCorrect(_ autoCorrect: Bool?) {
|
|
1810
|
+
richTextView.textView.setAutoCorrect(autoCorrect)
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
func setKeyboardType(_ keyboardType: String?) {
|
|
1814
|
+
richTextView.textView.setKeyboardType(keyboardType)
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1805
1817
|
func setShowToolbar(_ showToolbar: Bool) {
|
|
1806
1818
|
showsToolbar = showToolbar
|
|
1807
1819
|
updateAccessoryToolbarVisibility()
|
|
@@ -1913,7 +1925,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1913
1925
|
// MARK: - Focus Commands
|
|
1914
1926
|
|
|
1915
1927
|
func focus() {
|
|
1916
|
-
richTextView.textView.becomeFirstResponder()
|
|
1928
|
+
_ = richTextView.textView.becomeFirstResponder()
|
|
1917
1929
|
}
|
|
1918
1930
|
|
|
1919
1931
|
func blur() {
|
|
@@ -1956,7 +1968,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1956
1968
|
@objc private func textViewDidEndEditing(_ notification: Notification) {
|
|
1957
1969
|
if shouldPreserveFocusAfterToolbarTouch() {
|
|
1958
1970
|
DispatchQueue.main.async { [weak self] in
|
|
1959
|
-
self?.richTextView.textView.becomeFirstResponder()
|
|
1971
|
+
_ = self?.richTextView.textView.becomeFirstResponder()
|
|
1960
1972
|
}
|
|
1961
1973
|
return
|
|
1962
1974
|
}
|
|
@@ -340,6 +340,15 @@ public class NativeEditorModule: Module {
|
|
|
340
340
|
Prop("autoFocus") { (view: NativeEditorExpoView, autoFocus: Bool) in
|
|
341
341
|
view.setAutoFocus(autoFocus)
|
|
342
342
|
}
|
|
343
|
+
Prop("autoCapitalize") { (view: NativeEditorExpoView, autoCapitalize: String?) in
|
|
344
|
+
view.setAutoCapitalize(autoCapitalize)
|
|
345
|
+
}
|
|
346
|
+
Prop("autoCorrect") { (view: NativeEditorExpoView, autoCorrect: Bool?) in
|
|
347
|
+
view.setAutoCorrect(autoCorrect)
|
|
348
|
+
}
|
|
349
|
+
Prop("keyboardType") { (view: NativeEditorExpoView, keyboardType: String?) in
|
|
350
|
+
view.setKeyboardType(keyboardType)
|
|
351
|
+
}
|
|
343
352
|
Prop("showToolbar") { (view: NativeEditorExpoView, showToolbar: Bool) in
|
|
344
353
|
view.setShowToolbar(showToolbar)
|
|
345
354
|
}
|
|
@@ -819,6 +819,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
819
819
|
let entries: [TopLevelChildMetadata]
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
+
private struct NativeTextMutation {
|
|
823
|
+
let from: UInt32
|
|
824
|
+
let to: UInt32
|
|
825
|
+
let replacementText: String
|
|
826
|
+
let resultingText: String
|
|
827
|
+
}
|
|
828
|
+
|
|
822
829
|
private enum PositionCacheUpdate {
|
|
823
830
|
case scan
|
|
824
831
|
case invalidate
|
|
@@ -964,6 +971,8 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
964
971
|
/// trailing UIKit text-storage callbacks that arrive on the next run loop.
|
|
965
972
|
private var interceptedInputDepth = 0
|
|
966
973
|
private var reconciliationWorkScheduled = false
|
|
974
|
+
private var nativeTextMutationCommitScheduled = false
|
|
975
|
+
private var pendingNativeTextMutation: NativeTextMutation?
|
|
967
976
|
|
|
968
977
|
/// Coalesces selection sync until UIKit has finished resolving the
|
|
969
978
|
/// current tap/drag gesture's final caret position.
|
|
@@ -1031,9 +1040,9 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1031
1040
|
// Configure the text view as a Rust-controlled editor surface.
|
|
1032
1041
|
// UIKit smart-edit features mutate text storage outside our transaction
|
|
1033
1042
|
// pipeline and can race with stored-mark typing after toolbar actions.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1043
|
+
setAutoCorrect(nil)
|
|
1044
|
+
setAutoCapitalize(nil)
|
|
1045
|
+
setKeyboardType(nil)
|
|
1037
1046
|
smartQuotesType = .no
|
|
1038
1047
|
smartDashesType = .no
|
|
1039
1048
|
smartInsertDeleteType = .no
|
|
@@ -1063,6 +1072,63 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1063
1072
|
refreshNativeSelectionChromeVisibility()
|
|
1064
1073
|
}
|
|
1065
1074
|
|
|
1075
|
+
func setAutoCapitalize(_ autoCapitalize: String?) {
|
|
1076
|
+
switch autoCapitalize {
|
|
1077
|
+
case "none":
|
|
1078
|
+
autocapitalizationType = .none
|
|
1079
|
+
case "words":
|
|
1080
|
+
autocapitalizationType = .words
|
|
1081
|
+
case "characters":
|
|
1082
|
+
autocapitalizationType = .allCharacters
|
|
1083
|
+
default:
|
|
1084
|
+
autocapitalizationType = .sentences
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
func setAutoCorrect(_ autoCorrect: Bool?) {
|
|
1089
|
+
let isEnabled = autoCorrect ?? false
|
|
1090
|
+
autocorrectionType = isEnabled ? .yes : .no
|
|
1091
|
+
spellCheckingType = isEnabled ? .default : .no
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
func setKeyboardType(_ keyboardType: String?) {
|
|
1095
|
+
self.keyboardType = Self.resolvedKeyboardType(from: keyboardType)
|
|
1096
|
+
if isFirstResponder {
|
|
1097
|
+
reloadInputViews()
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
private static func resolvedKeyboardType(from keyboardType: String?) -> UIKeyboardType {
|
|
1102
|
+
switch keyboardType {
|
|
1103
|
+
case "ascii-capable":
|
|
1104
|
+
return .asciiCapable
|
|
1105
|
+
case "numbers-and-punctuation":
|
|
1106
|
+
return .numbersAndPunctuation
|
|
1107
|
+
case "url":
|
|
1108
|
+
return .URL
|
|
1109
|
+
case "number-pad":
|
|
1110
|
+
return .numberPad
|
|
1111
|
+
case "phone-pad":
|
|
1112
|
+
return .phonePad
|
|
1113
|
+
case "name-phone-pad":
|
|
1114
|
+
return .namePhonePad
|
|
1115
|
+
case "email-address":
|
|
1116
|
+
return .emailAddress
|
|
1117
|
+
case "decimal-pad", "numeric":
|
|
1118
|
+
return .decimalPad
|
|
1119
|
+
case "twitter":
|
|
1120
|
+
return .twitter
|
|
1121
|
+
case "web-search":
|
|
1122
|
+
return .webSearch
|
|
1123
|
+
case "ascii-capable-number-pad":
|
|
1124
|
+
return .asciiCapableNumberPad
|
|
1125
|
+
case "visible-password":
|
|
1126
|
+
return .asciiCapable
|
|
1127
|
+
default:
|
|
1128
|
+
return .default
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1066
1132
|
override func didMoveToWindow() {
|
|
1067
1133
|
super.didMoveToWindow()
|
|
1068
1134
|
installImageSelectionTapDependencies()
|
|
@@ -2152,7 +2218,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2152
2218
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
2153
2219
|
guard textView === self else { return }
|
|
2154
2220
|
ensureInternalTextViewDelegate()
|
|
2155
|
-
guard !isApplyingRustState, !isComposing else { return }
|
|
2221
|
+
guard !isApplyingRustState, !isComposing, !nativeTextMutationCommitScheduled else { return }
|
|
2156
2222
|
if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
|
|
2157
2223
|
return
|
|
2158
2224
|
}
|
|
@@ -2333,7 +2399,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2333
2399
|
}
|
|
2334
2400
|
|
|
2335
2401
|
private func syncSelectionToRustAndNotifyDelegate() {
|
|
2336
|
-
guard !isApplyingRustState,
|
|
2402
|
+
guard !isApplyingRustState,
|
|
2403
|
+
!isComposing,
|
|
2404
|
+
!nativeTextMutationCommitScheduled,
|
|
2405
|
+
editorId != 0
|
|
2406
|
+
else {
|
|
2407
|
+
return
|
|
2408
|
+
}
|
|
2337
2409
|
guard let range = selectedTextRange else { return }
|
|
2338
2410
|
|
|
2339
2411
|
let anchor = PositionBridge.textViewToScalar(range.start, in: self)
|
|
@@ -2940,6 +3012,94 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2940
3012
|
applyUpdateJSON(updateJSON)
|
|
2941
3013
|
}
|
|
2942
3014
|
|
|
3015
|
+
private func nativeTextMutationFromAuthorizedDiff(
|
|
3016
|
+
currentText: String
|
|
3017
|
+
) -> NativeTextMutation? {
|
|
3018
|
+
let authorizedText = lastAuthorizedText
|
|
3019
|
+
guard currentText != authorizedText else { return nil }
|
|
3020
|
+
|
|
3021
|
+
let authorized = authorizedText as NSString
|
|
3022
|
+
let current = currentText as NSString
|
|
3023
|
+
let sharedLength = min(authorized.length, current.length)
|
|
3024
|
+
var prefix = 0
|
|
3025
|
+
while prefix < sharedLength,
|
|
3026
|
+
authorized.character(at: prefix) == current.character(at: prefix) {
|
|
3027
|
+
prefix += 1
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
var authorizedEnd = authorized.length
|
|
3031
|
+
var currentEnd = current.length
|
|
3032
|
+
while authorizedEnd > prefix,
|
|
3033
|
+
currentEnd > prefix,
|
|
3034
|
+
authorized.character(at: authorizedEnd - 1) == current.character(at: currentEnd - 1) {
|
|
3035
|
+
authorizedEnd -= 1
|
|
3036
|
+
currentEnd -= 1
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
let replacementLength = currentEnd - prefix
|
|
3040
|
+
guard replacementLength >= 0 else { return nil }
|
|
3041
|
+
let replacementText = current.substring(
|
|
3042
|
+
with: NSRange(location: prefix, length: replacementLength)
|
|
3043
|
+
)
|
|
3044
|
+
|
|
3045
|
+
return NativeTextMutation(
|
|
3046
|
+
from: PositionBridge.utf16OffsetToScalar(prefix, in: authorizedText),
|
|
3047
|
+
to: PositionBridge.utf16OffsetToScalar(authorizedEnd, in: authorizedText),
|
|
3048
|
+
replacementText: replacementText,
|
|
3049
|
+
resultingText: currentText
|
|
3050
|
+
)
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
private func shouldAdoptNativeTextStorageMutation() -> Bool {
|
|
3054
|
+
isFirstResponder && isEditable
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
private func scheduleNativeTextMutationCommit(_ mutation: NativeTextMutation) {
|
|
3058
|
+
pendingNativeTextMutation = mutation
|
|
3059
|
+
guard !nativeTextMutationCommitScheduled else { return }
|
|
3060
|
+
|
|
3061
|
+
nativeTextMutationCommitScheduled = true
|
|
3062
|
+
DispatchQueue.main.async { [weak self] in
|
|
3063
|
+
guard let self else { return }
|
|
3064
|
+
self.nativeTextMutationCommitScheduled = false
|
|
3065
|
+
guard let mutation = self.pendingNativeTextMutation else { return }
|
|
3066
|
+
self.pendingNativeTextMutation = nil
|
|
3067
|
+
|
|
3068
|
+
guard self.editorId != 0,
|
|
3069
|
+
!self.isApplyingRustState,
|
|
3070
|
+
!self.isInterceptingInput,
|
|
3071
|
+
!self.isComposing,
|
|
3072
|
+
self.shouldAdoptNativeTextStorageMutation()
|
|
3073
|
+
else {
|
|
3074
|
+
if self.textStorage.string != self.lastAuthorizedText {
|
|
3075
|
+
self.scheduleReconciliationFromRust()
|
|
3076
|
+
}
|
|
3077
|
+
return
|
|
3078
|
+
}
|
|
3079
|
+
guard self.textStorage.string == mutation.resultingText else {
|
|
3080
|
+
if self.textStorage.string != self.lastAuthorizedText {
|
|
3081
|
+
self.scheduleReconciliationFromRust()
|
|
3082
|
+
}
|
|
3083
|
+
return
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
self.performInterceptedInput {
|
|
3087
|
+
if mutation.from == mutation.to {
|
|
3088
|
+
guard !mutation.replacementText.isEmpty else { return }
|
|
3089
|
+
self.insertTextInRust(mutation.replacementText, at: mutation.from)
|
|
3090
|
+
} else if mutation.replacementText.isEmpty {
|
|
3091
|
+
self.deleteScalarRangeInRust(from: mutation.from, to: mutation.to)
|
|
3092
|
+
} else {
|
|
3093
|
+
self.replaceTextRangeInRust(
|
|
3094
|
+
from: mutation.from,
|
|
3095
|
+
to: mutation.to,
|
|
3096
|
+
with: mutation.replacementText
|
|
3097
|
+
)
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
2943
3103
|
private func insertNodeInRust(_ nodeType: String) {
|
|
2944
3104
|
guard let selection = currentScalarSelection() else { return }
|
|
2945
3105
|
Self.inputLog.debug(
|
|
@@ -4386,6 +4546,13 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
4386
4546
|
let currentText = textStorage.string
|
|
4387
4547
|
guard currentText != lastAuthorizedText else { return }
|
|
4388
4548
|
currentTopLevelChildMetadata = nil
|
|
4549
|
+
|
|
4550
|
+
if shouldAdoptNativeTextStorageMutation(),
|
|
4551
|
+
let mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText) {
|
|
4552
|
+
scheduleNativeTextMutationCommit(mutation)
|
|
4553
|
+
return
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4389
4556
|
let authorizedPreview = preview(lastAuthorizedText)
|
|
4390
4557
|
let storagePreview = preview(currentText)
|
|
4391
4558
|
|
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.16",
|
|
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
|