@apollohg/react-native-prose-editor 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,8 @@
5
5
  This project is currently in `alpha` and the API, behavior, and packaging may still change.
6
6
 
7
7
  <p align="center">
8
- <img src="./docs/images/example-ios.png" alt="Example editor Android" width="45%" align="top" />
9
- <img src="./docs/images/example-android.png" alt="Example editor iOS" width="45%" align="top" />
8
+ <img src="https://raw.githubusercontent.com/wiki/apollohg/react-native-prose-editor/images/example-ios.png" alt="Example editor iOS" width="45%" align="top" />
9
+ <img src="https://raw.githubusercontent.com/wiki/apollohg/react-native-prose-editor/images/example-android.png" alt="Example editor Android" width="45%" align="top" />
10
10
  </p>
11
11
 
12
12
  This repository contains three main pieces:
@@ -38,7 +38,8 @@ The editor already supports:
38
38
  - [`android`](./android): Android native view, rendering bridge, and Expo module wiring
39
39
  - [`Rust Editor Core`](./rust/editor-core): document model, transforms, schema system, selection, history, serialization, and tests
40
40
  - [`example`](./example): Expo 54 app for manual QA and development
41
- - [`docs`](./docs): project documentation
41
+
42
+ Project documentation now lives in the [GitHub Wiki](https://github.com/apollohg/react-native-prose-editor/wiki).
42
43
 
43
44
  ## Installation
44
45
 
@@ -67,7 +68,7 @@ npm --prefix example install
67
68
  npm run example:prebuild
68
69
  ```
69
70
 
70
- For full setup details, including peer dependencies, example app setup, and iOS pods, see the [Installation Guide](./docs/guides/installation.md).
71
+ For full setup details, including peer dependencies, example app setup, and iOS pods, see the [Installation Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Installation).
71
72
 
72
73
  ## Basic Usage
73
74
 
@@ -106,9 +107,9 @@ The main extension points today are:
106
107
  - `addons`: configure optional features like @-mentions
107
108
  - `heightBehavior`: switch between internal scrolling and auto-grow
108
109
 
109
- For setup and customization details, start with the [Documentation Index](./docs/README.md).
110
+ For setup and customization details, start with the [Documentation Index](https://github.com/apollohg/react-native-prose-editor/wiki).
110
111
 
111
- For realtime collaboration, including the correct `useYjsCollaboration()` wiring, encoded-state persistence, remote cursors, and automatic reconnect behavior, see the [Collaboration Guide](./docs/modules/collaboration.md).
112
+ For realtime collaboration, including the correct `useYjsCollaboration()` wiring, encoded-state persistence, remote cursors, and automatic reconnect behavior, see the [Collaboration Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Collaboration).
112
113
 
113
114
  For whole-document JSON loads, `initialJSON`, controlled `valueJSON`, and `setContentJson()` will normalize an empty root document like `{ type: 'doc', content: [] }` to the active schema's empty text block so block-constrained schemas still load a valid empty document.
114
115
 
@@ -152,15 +153,17 @@ npm run ios:test:perf:device
152
153
 
153
154
  ## Documentation
154
155
 
155
- - [Documentation Index](./docs/README.md): main documentation index
156
- - [Installation Guide](./docs/guides/installation.md): installation and local setup
157
- - [Getting Started](./docs/guides/getting-started.md): first setup and first editor
158
- - [Collaboration Guide](./docs/modules/collaboration.md): Yjs collaboration wiring, source-of-truth rules, and persistence
159
- - [Toolbar Setup](./docs/guides/toolbar-setup.md): toolbar setup patterns and examples
160
- - [Mentions Guide](./docs/modules/mentions.md): @-mentions addon setup and configuration
161
- - [Styling Guide](./docs/guides/styling.md): content, toolbar, and mention styling
162
- - [NativeRichTextEditor Reference](./docs/reference/native-rich-text-editor.md): component props and ref methods
163
- - [Design Decisions](./docs/explanations/design-decisions.md): rationale for key API and architecture decisions
156
+ Documentation is published in the [GitHub Wiki](https://github.com/apollohg/react-native-prose-editor/wiki).
157
+
158
+ - [Documentation Index](https://github.com/apollohg/react-native-prose-editor/wiki): main documentation index
159
+ - [Installation Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Installation): installation and local setup
160
+ - [Getting Started](https://github.com/apollohg/react-native-prose-editor/wiki/Getting-Started): first setup and first editor
161
+ - [Collaboration Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Collaboration): Yjs collaboration wiring, source-of-truth rules, and persistence
162
+ - [Toolbar Setup](https://github.com/apollohg/react-native-prose-editor/wiki/Toolbar-Setup): toolbar setup patterns and examples
163
+ - [Mentions Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Mentions): @-mentions addon setup and configuration
164
+ - [Styling Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Styling): content, toolbar, and mention styling
165
+ - [NativeRichTextEditor Reference](https://github.com/apollohg/react-native-prose-editor/wiki/NativeRichTextEditor-Reference): component props and ref methods
166
+ - [Design Decisions](https://github.com/apollohg/react-native-prose-editor/wiki/Design-Decisions): rationale for key API and architecture decisions
164
167
 
165
168
  ## Project Status
166
169
 
@@ -30,7 +30,8 @@ data class NativeMentionSuggestion(
30
30
  data class NativeMentionsAddonConfig(
31
31
  val trigger: String,
32
32
  val suggestions: List<NativeMentionSuggestion>,
33
- val theme: EditorMentionTheme?
33
+ val theme: EditorMentionTheme?,
34
+ val resolveSelectionAttrs: Boolean
34
35
  ) {
35
36
  companion object {
36
37
  fun fromJson(json: JSONObject?): NativeMentionsAddonConfig? {
@@ -51,7 +52,8 @@ data class NativeMentionsAddonConfig(
51
52
  return NativeMentionsAddonConfig(
52
53
  trigger = trigger,
53
54
  suggestions = suggestions,
54
- theme = EditorMentionTheme.fromJson(json.optJSONObject("theme"))
55
+ theme = EditorMentionTheme.fromJson(json.optJSONObject("theme")),
56
+ resolveSelectionAttrs = json.optBoolean("resolveSelectionAttrs", false)
55
57
  )
56
58
  }
57
59
  }
@@ -127,6 +127,29 @@ data class EditorMentionTheme(
127
127
  val optionHighlightedBackgroundColor: Int? = null,
128
128
  val optionHighlightedTextColor: Int? = null
129
129
  ) {
130
+ fun mergedWith(other: EditorMentionTheme?): EditorMentionTheme {
131
+ other ?: return this
132
+ return copy(
133
+ textColor = other.textColor ?: textColor,
134
+ backgroundColor = other.backgroundColor ?: backgroundColor,
135
+ borderColor = other.borderColor ?: borderColor,
136
+ borderWidth = other.borderWidth ?: borderWidth,
137
+ borderRadius = other.borderRadius ?: borderRadius,
138
+ fontWeight = other.fontWeight ?: fontWeight,
139
+ popoverBackgroundColor = other.popoverBackgroundColor ?: popoverBackgroundColor,
140
+ popoverBorderColor = other.popoverBorderColor ?: popoverBorderColor,
141
+ popoverBorderWidth = other.popoverBorderWidth ?: popoverBorderWidth,
142
+ popoverBorderRadius = other.popoverBorderRadius ?: popoverBorderRadius,
143
+ popoverShadowColor = other.popoverShadowColor ?: popoverShadowColor,
144
+ optionTextColor = other.optionTextColor ?: optionTextColor,
145
+ optionSecondaryTextColor = other.optionSecondaryTextColor ?: optionSecondaryTextColor,
146
+ optionHighlightedBackgroundColor =
147
+ other.optionHighlightedBackgroundColor ?: optionHighlightedBackgroundColor,
148
+ optionHighlightedTextColor =
149
+ other.optionHighlightedTextColor ?: optionHighlightedTextColor
150
+ )
151
+ }
152
+
130
153
  companion object {
131
154
  fun fromJson(json: JSONObject?): EditorMentionTheme? {
132
155
  json ?: return null
@@ -529,12 +529,42 @@ class NativeEditorExpoView(
529
529
  onAddonEvent(mapOf("eventJson" to eventJson))
530
530
  }
531
531
 
532
- private fun emitMentionSelect(trigger: String, suggestion: NativeMentionSuggestion) {
532
+ private fun resolvedMentionAttrs(
533
+ trigger: String,
534
+ suggestion: NativeMentionSuggestion
535
+ ): JSONObject {
536
+ val attrs = JSONObject(suggestion.attrs.toString())
537
+ if (!attrs.has("label")) {
538
+ attrs.put("label", suggestion.label)
539
+ }
540
+ if (!attrs.has("mentionSuggestionChar")) {
541
+ attrs.put("mentionSuggestionChar", trigger)
542
+ }
543
+ return attrs
544
+ }
545
+
546
+ private fun emitMentionSelect(trigger: String, suggestion: NativeMentionSuggestion, attrs: JSONObject) {
533
547
  val eventJson = JSONObject()
534
548
  .put("type", "mentionsSelect")
535
549
  .put("trigger", trigger)
536
550
  .put("suggestionKey", suggestion.key)
537
- .put("attrs", suggestion.attrs)
551
+ .put("attrs", attrs)
552
+ .toString()
553
+ onAddonEvent(mapOf("eventJson" to eventJson))
554
+ }
555
+
556
+ private fun emitMentionSelectRequest(
557
+ trigger: String,
558
+ suggestion: NativeMentionSuggestion,
559
+ attrs: JSONObject,
560
+ range: MentionQueryState
561
+ ) {
562
+ val eventJson = JSONObject()
563
+ .put("type", "mentionsSelectRequest")
564
+ .put("trigger", trigger)
565
+ .put("suggestionKey", suggestion.key)
566
+ .put("attrs", attrs)
567
+ .put("range", JSONObject().put("anchor", range.anchor).put("head", range.head))
538
568
  .toString()
539
569
  onAddonEvent(mapOf("eventJson" to eventJson))
540
570
  }
@@ -542,9 +572,12 @@ class NativeEditorExpoView(
542
572
  private fun insertMentionSuggestion(suggestion: NativeMentionSuggestion) {
543
573
  val mentions = addons.mentions ?: return
544
574
  val queryState = mentionQueryState ?: return
545
- val attrs = JSONObject(suggestion.attrs.toString())
546
- if (!attrs.has("label")) {
547
- attrs.put("label", suggestion.label)
575
+ val attrs = resolvedMentionAttrs(mentions.trigger, suggestion)
576
+ if (mentions.resolveSelectionAttrs) {
577
+ emitMentionSelectRequest(mentions.trigger, suggestion, attrs, queryState)
578
+ lastMentionEventJson = null
579
+ clearMentionQueryState()
580
+ return
548
581
  }
549
582
  val docJson = JSONObject()
550
583
  .put("type", "doc")
@@ -564,7 +597,7 @@ class NativeEditorExpoView(
564
597
  docJson.toString()
565
598
  )
566
599
  richTextView.editorEditText.applyUpdateJSON(updateJson)
567
- emitMentionSelect(mentions.trigger, suggestion)
600
+ emitMentionSelect(mentions.trigger, suggestion, attrs)
568
601
  lastMentionEventJson = null
569
602
  clearMentionQueryState()
570
603
  }
@@ -909,6 +909,9 @@ object RenderBridge {
909
909
  val nodeType = element.optString("nodeType", "")
910
910
  val label = element.optString("label", "?")
911
911
  val docPos = element.optInt("docPos", 0)
912
+ val mentionTheme = EditorMentionTheme.fromJson(
913
+ element.optJSONObject("mentionTheme")
914
+ )
912
915
  appendOpaqueInlineAtom(
913
916
  state.result,
914
917
  nodeType,
@@ -919,6 +922,7 @@ object RenderBridge {
919
922
  state.blockStack,
920
923
  state.pendingLeadingMargins,
921
924
  theme,
925
+ mentionTheme,
922
926
  density
923
927
  )
924
928
  }
@@ -1353,6 +1357,7 @@ object RenderBridge {
1353
1357
  blockStack: MutableList<BlockContext>,
1354
1358
  pendingLeadingMargins: MutableMap<Int, PendingLeadingMargin>,
1355
1359
  theme: EditorTheme?,
1360
+ mentionTheme: EditorMentionTheme?,
1356
1361
  density: Float
1357
1362
  ) {
1358
1363
  val isMention = nodeType == "mention"
@@ -1360,8 +1365,13 @@ object RenderBridge {
1360
1365
  val start = builder.length
1361
1366
  builder.append(text)
1362
1367
  val end = builder.length
1368
+ val resolvedMentionTheme = if (isMention) {
1369
+ theme?.mentions?.mergedWith(mentionTheme) ?: mentionTheme
1370
+ } else {
1371
+ null
1372
+ }
1363
1373
  val inlineTextColor = if (isMention) {
1364
- theme?.mentions?.textColor ?: resolveInlineTextColor(blockStack, textColor, theme)
1374
+ resolvedMentionTheme?.textColor ?: resolveInlineTextColor(blockStack, textColor, theme)
1365
1375
  } else {
1366
1376
  resolveInlineTextColor(blockStack, textColor, theme)
1367
1377
  }
@@ -1372,7 +1382,7 @@ object RenderBridge {
1372
1382
  builder.setSpan(
1373
1383
  BackgroundColorSpan(
1374
1384
  if (isMention) {
1375
- theme?.mentions?.backgroundColor ?: 0x1f1d4ed8
1385
+ resolvedMentionTheme?.backgroundColor ?: 0x1f1d4ed8
1376
1386
  } else {
1377
1387
  0x20000000
1378
1388
  }
@@ -1387,8 +1397,8 @@ object RenderBridge {
1387
1397
  Annotation("nativeDocPos", docPos.toString()),
1388
1398
  start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1389
1399
  )
1390
- if (isMention && (theme?.mentions?.fontWeight == "bold" ||
1391
- theme?.mentions?.fontWeight?.toIntOrNull()?.let { it >= 600 } == true)
1400
+ if (isMention && (resolvedMentionTheme?.fontWeight == "bold" ||
1401
+ resolvedMentionTheme?.fontWeight?.toIntOrNull()?.let { it >= 600 } == true)
1392
1402
  ) {
1393
1403
  builder.setSpan(
1394
1404
  StyleSpan(Typeface.BOLD),
@@ -1,3 +1,4 @@
1
+ import type { EditorMentionTheme } from './EditorTheme';
1
2
  import { type SchemaDefinition } from './schemas';
2
3
  export interface NativeEditorModule {
3
4
  editorCreate(configJson: string): number;
@@ -93,6 +94,7 @@ export interface RenderElement {
93
94
  docPos?: number;
94
95
  label?: string;
95
96
  attrs?: Record<string, unknown>;
97
+ mentionTheme?: EditorMentionTheme;
96
98
  listContext?: ListContext;
97
99
  }
98
100
  interface RenderBlocksPatch {
@@ -194,6 +196,8 @@ export declare class NativeEditorBridge {
194
196
  toggleMark(markType: string): EditorUpdate | null;
195
197
  /** Set a mark with attrs on the current selection. */
196
198
  setMark(markType: string, attrs: Record<string, unknown>): EditorUpdate | null;
199
+ /** Set a mark with attrs at an explicit scalar selection. */
200
+ setMarkAtSelectionScalar(scalarAnchor: number, scalarHead: number, markType: string, attrs: Record<string, unknown>): EditorUpdate | null;
197
201
  /** Remove a mark from the current selection. */
198
202
  unsetMark(markType: string): EditorUpdate | null;
199
203
  /** Toggle blockquote wrapping for the current block selection. */
@@ -202,6 +206,10 @@ export declare class NativeEditorBridge {
202
206
  toggleHeading(level: number): EditorUpdate | null;
203
207
  /** Set the document selection by anchor and head positions. */
204
208
  setSelection(anchor: number, head: number): void;
209
+ /** Convert a document position to a scalar position used by native text views. */
210
+ docToScalar(docPos: number): number;
211
+ /** Convert a native scalar position back to a document position. */
212
+ scalarToDoc(scalar: number): number;
205
213
  /** Get the current selection from the Rust engine (synchronous native call).
206
214
  * Always returns the live selection, not a stale cache. */
207
215
  getSelection(): Selection;
@@ -433,6 +433,12 @@ class NativeEditorBridge {
433
433
  : getNativeModule().editorSetMark(this._editorId, markType, attrsJson);
434
434
  return this.parseAndNoteUpdate(json);
435
435
  }
436
+ /** Set a mark with attrs at an explicit scalar selection. */
437
+ setMarkAtSelectionScalar(scalarAnchor, scalarHead, markType, attrs) {
438
+ this.assertNotDestroyed();
439
+ const json = getNativeModule().editorSetMarkAtSelectionScalar(this._editorId, scalarAnchor, scalarHead, markType, JSON.stringify(attrs));
440
+ return this.parseAndNoteUpdate(json);
441
+ }
436
442
  /** Remove a mark from the current selection. */
437
443
  unsetMark(markType) {
438
444
  this.assertNotDestroyed();
@@ -469,6 +475,16 @@ class NativeEditorBridge {
469
475
  getNativeModule().editorSetSelection(this._editorId, anchor, head);
470
476
  this._lastSelection = { type: 'text', anchor, head };
471
477
  }
478
+ /** Convert a document position to a scalar position used by native text views. */
479
+ docToScalar(docPos) {
480
+ this.assertNotDestroyed();
481
+ return getNativeModule().editorDocToScalar(this._editorId, docPos);
482
+ }
483
+ /** Convert a native scalar position back to a document position. */
484
+ scalarToDoc(scalar) {
485
+ this.assertNotDestroyed();
486
+ return getNativeModule().editorScalarToDoc(this._editorId, scalar);
487
+ }
472
488
  /** Get the current selection from the Rust engine (synchronous native call).
473
489
  * Always returns the live selection, not a stale cache. */
474
490
  getSelection() {
@@ -1,12 +1,14 @@
1
1
  import { type StyleProp, type ViewStyle } from 'react-native';
2
- import { type EditorTheme } from './EditorTheme';
2
+ import { type EditorMentionTheme, type EditorTheme } from './EditorTheme';
3
3
  import type { DocumentJSON } from './NativeEditorBridge';
4
4
  import { type SchemaDefinition } from './schemas';
5
- export interface NativeProseViewerMentionPressEvent {
5
+ export interface NativeProseViewerMentionRenderContext {
6
6
  docPos: number;
7
7
  label: string;
8
8
  attrs: Record<string, unknown>;
9
9
  }
10
+ export interface NativeProseViewerMentionPressEvent extends NativeProseViewerMentionRenderContext {
11
+ }
10
12
  type NativeProseViewerContent = DocumentJSON | string;
11
13
  export interface NativeProseViewerProps {
12
14
  contentJSON: NativeProseViewerContent;
@@ -15,7 +17,9 @@ export interface NativeProseViewerProps {
15
17
  theme?: EditorTheme;
16
18
  style?: StyleProp<ViewStyle>;
17
19
  allowBase64Images?: boolean;
20
+ mentionPrefix?: string | ((mention: NativeProseViewerMentionRenderContext) => string | null | undefined);
21
+ resolveMentionTheme?: (mention: NativeProseViewerMentionRenderContext) => EditorMentionTheme | null | undefined;
18
22
  onPressMention?: (event: NativeProseViewerMentionPressEvent) => void;
19
23
  }
20
- export declare function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, style, allowBase64Images, onPressMention, }: NativeProseViewerProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, style, allowBase64Images, mentionPrefix, resolveMentionTheme, onPressMention, }: NativeProseViewerProps): import("react/jsx-runtime").JSX.Element;
21
25
  export {};
@@ -56,7 +56,21 @@ function normalizeMentionAttrs(node) {
56
56
  }
57
57
  return attrs;
58
58
  }
59
- function collectMentionPayloadsByDocPos(document) {
59
+ function baseMentionLabelFromAttrs(attrs) {
60
+ const label = attrs.label;
61
+ return typeof label === 'string' && label.length > 0 ? label : 'mention';
62
+ }
63
+ function resolveMentionPrefix(mentionPrefix, mention) {
64
+ const rawPrefix = typeof mentionPrefix === 'function' ? mentionPrefix(mention) : mentionPrefix;
65
+ return typeof rawPrefix === 'string' && rawPrefix.length > 0 ? rawPrefix : undefined;
66
+ }
67
+ function applyMentionPrefix(label, prefix) {
68
+ if (!prefix || label.startsWith(prefix)) {
69
+ return label;
70
+ }
71
+ return `${prefix}${label}`;
72
+ }
73
+ function collectMentionPayloadsByDocPos(document, mentionPrefix, resolveMentionTheme) {
60
74
  const mentions = new Map();
61
75
  const visit = (node, pos, isRoot = false) => {
62
76
  if (node == null || typeof node !== 'object') {
@@ -71,8 +85,15 @@ function collectMentionPayloadsByDocPos(document) {
71
85
  }
72
86
  if (nodeType === 'mention') {
73
87
  const attrs = normalizeMentionAttrs(nodeRecord);
74
- const label = typeof attrs.label === 'string' ? attrs.label : undefined;
75
- mentions.set(pos, { label, attrs });
88
+ const label = baseMentionLabelFromAttrs(attrs);
89
+ const mentionContext = { docPos: pos, label, attrs };
90
+ const renderedLabel = applyMentionPrefix(label, resolveMentionPrefix(mentionPrefix, mentionContext));
91
+ const mentionTheme = resolveMentionTheme?.(mentionContext) ?? undefined;
92
+ mentions.set(pos, {
93
+ ...mentionContext,
94
+ renderedLabel,
95
+ mentionTheme,
96
+ });
76
97
  }
77
98
  if (isRoot && nodeType === 'doc') {
78
99
  let nextPos = pos;
@@ -93,6 +114,50 @@ function collectMentionPayloadsByDocPos(document) {
93
114
  visit(document, 0, true);
94
115
  return mentions;
95
116
  }
117
+ function applyResolvedMentionRendering(renderJson, mentionPayloadsByDocPos) {
118
+ if (mentionPayloadsByDocPos.size === 0) {
119
+ return renderJson;
120
+ }
121
+ let parsedElements;
122
+ try {
123
+ parsedElements = JSON.parse(renderJson);
124
+ }
125
+ catch {
126
+ return renderJson;
127
+ }
128
+ if (!Array.isArray(parsedElements)) {
129
+ return renderJson;
130
+ }
131
+ let didChange = false;
132
+ const nextElements = parsedElements.map((element) => {
133
+ if (element == null || typeof element !== 'object' || Array.isArray(element)) {
134
+ return element;
135
+ }
136
+ const renderElement = element;
137
+ if (renderElement.type !== 'opaqueInlineAtom' ||
138
+ renderElement.nodeType !== 'mention' ||
139
+ typeof renderElement.docPos !== 'number') {
140
+ return element;
141
+ }
142
+ const mention = mentionPayloadsByDocPos.get(renderElement.docPos);
143
+ if (!mention) {
144
+ return element;
145
+ }
146
+ let nextElement = renderElement;
147
+ if (renderElement.label !== mention.renderedLabel) {
148
+ nextElement = { ...nextElement, label: mention.renderedLabel };
149
+ didChange = true;
150
+ }
151
+ if (mention.mentionTheme && Object.keys(mention.mentionTheme).length > 0) {
152
+ nextElement =
153
+ nextElement === renderElement ? { ...nextElement } : nextElement;
154
+ nextElement.mentionTheme = mention.mentionTheme;
155
+ didChange = true;
156
+ }
157
+ return nextElement;
158
+ });
159
+ return didChange ? JSON.stringify(nextElements) : renderJson;
160
+ }
96
161
  function serializeDocumentInput(document, schema) {
97
162
  if (typeof document === 'string') {
98
163
  try {
@@ -129,13 +194,13 @@ function extractRenderError(json) {
129
194
  return null;
130
195
  }
131
196
  }
132
- function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, style, allowBase64Images = false, onPressMention, }) {
197
+ function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, style, allowBase64Images = false, mentionPrefix, resolveMentionTheme, onPressMention, }) {
133
198
  const documentSchema = (0, react_1.useMemo)(() => (0, addons_1.withMentionsSchema)(schema ?? schemas_1.tiptapSchema), [schema]);
134
199
  const { normalizedDocument, serializedContentJson } = (0, react_1.useMemo)(() => serializeDocumentInput(contentJSON, documentSchema), [contentJSON, contentJSONRevision, documentSchema]);
135
200
  const themeJson = (0, react_1.useMemo)(() => (0, EditorTheme_1.serializeEditorTheme)(theme), [theme]);
136
201
  const mentionPayloadsByDocPos = (0, react_1.useMemo)(() => normalizedDocument == null
137
202
  ? new Map()
138
- : collectMentionPayloadsByDocPos(normalizedDocument), [normalizedDocument]);
203
+ : collectMentionPayloadsByDocPos(normalizedDocument, mentionPrefix, resolveMentionTheme), [mentionPrefix, normalizedDocument, resolveMentionTheme]);
139
204
  const renderJson = (0, react_1.useMemo)(() => {
140
205
  const configJson = JSON.stringify({
141
206
  schema: documentSchema,
@@ -148,15 +213,35 @@ function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, st
148
213
  return '[]';
149
214
  }
150
215
  if (looksLikeRenderElementsJson(nextRenderJson)) {
151
- return nextRenderJson;
216
+ return applyResolvedMentionRendering(nextRenderJson, mentionPayloadsByDocPos);
152
217
  }
153
218
  console.error('NativeProseViewer: native renderDocumentJson returned an invalid payload.');
154
219
  return '[]';
155
- }, [allowBase64Images, documentSchema, serializedContentJson]);
220
+ }, [
221
+ allowBase64Images,
222
+ documentSchema,
223
+ mentionPayloadsByDocPos,
224
+ serializedContentJson,
225
+ ]);
156
226
  const [contentHeight, setContentHeight] = (0, react_1.useState)(null);
227
+ const allowContentHeightShrinkRef = (0, react_1.useRef)(true);
228
+ (0, react_1.useEffect)(() => {
229
+ allowContentHeightShrinkRef.current = true;
230
+ }, [contentJSONRevision, renderJson, themeJson]);
157
231
  const handleContentHeightChange = (0, react_1.useCallback)((event) => {
158
232
  const nextHeight = event.nativeEvent.contentHeight;
159
- setContentHeight((currentHeight) => currentHeight === nextHeight ? currentHeight : nextHeight);
233
+ if (nextHeight <= 0)
234
+ return;
235
+ setContentHeight((currentHeight) => currentHeight == null ||
236
+ nextHeight >= currentHeight ||
237
+ allowContentHeightShrinkRef.current
238
+ ? (() => {
239
+ allowContentHeightShrinkRef.current = false;
240
+ return currentHeight === nextHeight
241
+ ? currentHeight
242
+ : nextHeight;
243
+ })()
244
+ : currentHeight);
160
245
  }, []);
161
246
  const handlePressMention = (0, react_1.useCallback)((event) => {
162
247
  if (!onPressMention)
@@ -165,10 +250,14 @@ function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, st
165
250
  const resolvedMention = mentionPayloadsByDocPos.get(docPos);
166
251
  onPressMention({
167
252
  docPos,
168
- label: resolvedMention?.label ?? label,
253
+ label: resolvedMention?.renderedLabel ?? label,
169
254
  attrs: resolvedMention?.attrs ?? {},
170
255
  });
171
256
  }, [mentionPayloadsByDocPos, onPressMention]);
172
- const nativeStyle = (0, react_1.useMemo)(() => [{ minHeight: 1 }, style, contentHeight != null ? { height: contentHeight } : null], [contentHeight, style]);
257
+ const nativeStyle = (0, react_1.useMemo)(() => [
258
+ { minHeight: 1 },
259
+ style,
260
+ contentHeight != null ? { minHeight: contentHeight } : null,
261
+ ], [contentHeight, style]);
173
262
  return ((0, jsx_runtime_1.jsx)(NativeProseViewerView, { style: nativeStyle, renderJson: renderJson, themeJson: themeJson, onContentHeightChange: handleContentHeightChange, onPressMention: typeof onPressMention === 'function' ? handlePressMention : undefined }));
174
263
  }
@@ -63,6 +63,8 @@ export interface NativeRichTextEditorProps {
63
63
  onRequestLink?: (context: LinkRequestContext) => void;
64
64
  /** Called when a toolbar image item is pressed so the host can choose an image source. */
65
65
  onRequestImage?: (context: ImageRequestContext) => void;
66
+ /** Whether plain URLs typed or pasted into the editor should be converted into link marks automatically. */
67
+ autoDetectLinks?: boolean;
66
68
  /** Whether `data:image/...` sources are accepted for image insertion and HTML parsing. */
67
69
  allowBase64Images?: boolean;
68
70
  /** Whether selected images show native resize handles. */