@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.
@@ -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 = EditorInfo.TYPE_CLASS_TEXT or
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
  }
@@ -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 {};
@@ -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, contentContainerStyle: styles.scrollContent, keyboardShouldPersistTaps: 'always', onScrollBeginDrag: () => setMenuState(null), children: renderedItems.map((item) => {
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
- ? (inlineToolbarMentionTheme?.optionHighlightedBackgroundColor ??
1413
+ ? (suggestionTheme?.optionHighlightedBackgroundColor ??
1323
1414
  'rgba(0, 122, 255, 0.12)')
1324
- : (inlineToolbarMentionTheme?.backgroundColor ??
1415
+ : (suggestionTheme?.backgroundColor ??
1325
1416
  '#F2F2F7'),
1326
- borderColor: inlineToolbarMentionTheme?.borderColor ??
1417
+ borderColor: suggestionTheme?.borderColor ??
1327
1418
  'transparent',
1328
- borderWidth: inlineToolbarMentionTheme?.borderWidth ?? 0,
1329
- borderRadius: inlineToolbarMentionTheme?.borderRadius ?? 12,
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
- ? (inlineToolbarMentionTheme?.optionHighlightedTextColor ??
1336
- inlineToolbarMentionTheme?.optionTextColor ??
1428
+ ? (suggestionTheme?.optionHighlightedTextColor ??
1429
+ suggestionTheme?.optionTextColor ??
1337
1430
  '#000000')
1338
- : (inlineToolbarMentionTheme?.optionTextColor ??
1339
- inlineToolbarMentionTheme?.textColor ??
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: inlineToolbarMentionTheme?.optionSecondaryTextColor ??
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';
@@ -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
- autocorrectionType = .no
1035
- autocapitalizationType = .sentences
1036
- spellCheckingType = .no
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, !isComposing, editorId != 0 else { return }
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.14",
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",