@apollohg/react-native-prose-editor 0.5.0 → 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 +19 -16
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +4 -2
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +39 -6
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +14 -4
- package/dist/NativeEditorBridge.d.ts +8 -0
- package/dist/NativeEditorBridge.js +16 -0
- package/dist/NativeProseViewer.d.ts +7 -3
- package/dist/NativeProseViewer.js +99 -10
- package/dist/NativeRichTextEditor.d.ts +2 -0
- package/dist/NativeRichTextEditor.js +417 -31
- package/dist/addons.d.ts +20 -0
- package/dist/addons.js +4 -0
- package/dist/index.d.ts +2 -2
- package/ios/EditorAddons.swift +2 -0
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +10 -1
- package/ios/EditorTheme.swift +25 -0
- package/ios/NativeEditorExpoView.swift +56 -6
- package/ios/NativeProseViewerExpoView.swift +6 -3
- package/ios/RenderBridge.swift +32 -14
- package/ios/RichTextEditorView.swift +4 -0
- package/package.json +4 -6
- 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
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="
|
|
9
|
-
<img src="
|
|
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
|
-
|
|
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
|
|
|
@@ -56,7 +57,7 @@ Required peer dependencies:
|
|
|
56
57
|
Install the package:
|
|
57
58
|
|
|
58
59
|
```sh
|
|
59
|
-
npm install @apollohg/react-native-prose-editor@0.5.
|
|
60
|
+
npm install @apollohg/react-native-prose-editor@0.5.1
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
For local package development in this repo:
|
|
@@ -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](
|
|
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](
|
|
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](
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
- [
|
|
158
|
-
- [
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
161
|
-
- [
|
|
162
|
-
- [
|
|
163
|
-
- [
|
|
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
|
|
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",
|
|
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 =
|
|
546
|
-
if (
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 && (
|
|
1391
|
-
|
|
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
|
|
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
|
|
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 =
|
|
75
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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?.
|
|
253
|
+
label: resolvedMention?.renderedLabel ?? label,
|
|
169
254
|
attrs: resolvedMention?.attrs ?? {},
|
|
170
255
|
});
|
|
171
256
|
}, [mentionPayloadsByDocPos, onPressMention]);
|
|
172
|
-
const nativeStyle = (0, react_1.useMemo)(() => [
|
|
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. */
|