@apollohg/react-native-prose-editor 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +4 -2
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +36 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +10 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +102 -37
- package/dist/EditorTheme.d.ts +10 -0
- package/dist/NativeProseViewer.d.ts +14 -3
- package/dist/NativeProseViewer.js +61 -13
- package/dist/NativeRichTextEditor.js +20 -4
- package/dist/addons.d.ts +3 -0
- package/dist/addons.js +1 -0
- package/dist/heightCache.d.ts +6 -0
- package/dist/heightCache.js +45 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -1
- package/ios/EditorAddons.swift +2 -0
- 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/EditorTheme.swift +33 -0
- package/ios/NativeEditorExpoView.swift +1 -1
- package/ios/NativeEditorModule.swift +8 -0
- package/ios/RenderBridge.swift +81 -10
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
|
@@ -31,7 +31,8 @@ data class NativeMentionsAddonConfig(
|
|
|
31
31
|
val trigger: String,
|
|
32
32
|
val suggestions: List<NativeMentionSuggestion>,
|
|
33
33
|
val theme: EditorMentionTheme?,
|
|
34
|
-
val resolveSelectionAttrs: Boolean
|
|
34
|
+
val resolveSelectionAttrs: Boolean,
|
|
35
|
+
val resolveTheme: Boolean
|
|
35
36
|
) {
|
|
36
37
|
companion object {
|
|
37
38
|
fun fromJson(json: JSONObject?): NativeMentionsAddonConfig? {
|
|
@@ -53,7 +54,8 @@ data class NativeMentionsAddonConfig(
|
|
|
53
54
|
trigger = trigger,
|
|
54
55
|
suggestions = suggestions,
|
|
55
56
|
theme = EditorMentionTheme.fromJson(json.optJSONObject("theme")),
|
|
56
|
-
resolveSelectionAttrs = json.optBoolean("resolveSelectionAttrs", false)
|
|
57
|
+
resolveSelectionAttrs = json.optBoolean("resolveSelectionAttrs", false),
|
|
58
|
+
resolveTheme = json.optBoolean("resolveTheme", false)
|
|
57
59
|
)
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -112,6 +112,40 @@ data class EditorBlockquoteTheme(
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
data class EditorLinkTheme(
|
|
116
|
+
val fontFamily: String? = null,
|
|
117
|
+
val fontSize: Float? = null,
|
|
118
|
+
val fontWeight: String? = null,
|
|
119
|
+
val fontStyle: String? = null,
|
|
120
|
+
val color: Int? = null,
|
|
121
|
+
val backgroundColor: Int? = null,
|
|
122
|
+
val underline: Boolean? = null
|
|
123
|
+
) {
|
|
124
|
+
companion object {
|
|
125
|
+
fun fromJson(json: JSONObject?): EditorLinkTheme? {
|
|
126
|
+
json ?: return null
|
|
127
|
+
return EditorLinkTheme(
|
|
128
|
+
fontFamily = json.optNullableString("fontFamily"),
|
|
129
|
+
fontSize = json.optNullableFloat("fontSize"),
|
|
130
|
+
fontWeight = json.optNullableString("fontWeight"),
|
|
131
|
+
fontStyle = json.optNullableString("fontStyle"),
|
|
132
|
+
color = parseColor(json.optNullableString("color")),
|
|
133
|
+
backgroundColor = parseColor(json.optNullableString("backgroundColor")),
|
|
134
|
+
underline = if (json.has("underline")) json.optBoolean("underline") else null
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fun asTextStyle(): EditorTextStyle =
|
|
140
|
+
EditorTextStyle(
|
|
141
|
+
fontFamily = fontFamily,
|
|
142
|
+
fontSize = fontSize,
|
|
143
|
+
fontWeight = fontWeight,
|
|
144
|
+
fontStyle = fontStyle,
|
|
145
|
+
color = color
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
115
149
|
data class EditorMentionTheme(
|
|
116
150
|
val textColor: Int? = null,
|
|
117
151
|
val backgroundColor: Int? = null,
|
|
@@ -268,6 +302,7 @@ data class EditorTheme(
|
|
|
268
302
|
val list: EditorListTheme? = null,
|
|
269
303
|
val horizontalRule: EditorHorizontalRuleTheme? = null,
|
|
270
304
|
val mentions: EditorMentionTheme? = null,
|
|
305
|
+
val links: EditorLinkTheme? = null,
|
|
271
306
|
val toolbar: EditorToolbarTheme? = null,
|
|
272
307
|
val backgroundColor: Int? = null,
|
|
273
308
|
val borderRadius: Float? = null,
|
|
@@ -298,6 +333,7 @@ data class EditorTheme(
|
|
|
298
333
|
list = EditorListTheme.fromJson(root.optJSONObject("list")),
|
|
299
334
|
horizontalRule = EditorHorizontalRuleTheme.fromJson(root.optJSONObject("horizontalRule")),
|
|
300
335
|
mentions = EditorMentionTheme.fromJson(root.optJSONObject("mentions")),
|
|
336
|
+
links = EditorLinkTheme.fromJson(root.optJSONObject("links")),
|
|
301
337
|
toolbar = EditorToolbarTheme.fromJson(root.optJSONObject("toolbar")),
|
|
302
338
|
backgroundColor = parseColor(root.optNullableString("backgroundColor")),
|
|
303
339
|
borderRadius = root.optNullableFloat("borderRadius"),
|
|
@@ -573,7 +573,7 @@ class NativeEditorExpoView(
|
|
|
573
573
|
val mentions = addons.mentions ?: return
|
|
574
574
|
val queryState = mentionQueryState ?: return
|
|
575
575
|
val attrs = resolvedMentionAttrs(mentions.trigger, suggestion)
|
|
576
|
-
if (mentions.resolveSelectionAttrs) {
|
|
576
|
+
if (mentions.resolveSelectionAttrs || mentions.resolveTheme) {
|
|
577
577
|
emitMentionSelectRequest(mentions.trigger, suggestion, attrs, queryState)
|
|
578
578
|
lastMentionEventJson = null
|
|
579
579
|
clearMentionQueryState()
|
|
@@ -295,6 +295,16 @@ class NativeEditorModule : Module() {
|
|
|
295
295
|
editorDestroy(editorId)
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
+
Function("measureContentHeight") { renderJson: String, themeJson: String?, width: Double ->
|
|
299
|
+
val density = appContext.reactContext?.resources?.displayMetrics?.density ?: 1f
|
|
300
|
+
val height = RenderBridge.measureHeight(
|
|
301
|
+
json = renderJson,
|
|
302
|
+
themeJson = themeJson,
|
|
303
|
+
width = width.toFloat(),
|
|
304
|
+
density = density
|
|
305
|
+
)
|
|
306
|
+
height.toDouble()
|
|
307
|
+
}
|
|
298
308
|
Function("renderDocumentHtml") { configJson: String, html: String ->
|
|
299
309
|
val editorId = editorCreate(configJson)
|
|
300
310
|
try {
|
|
@@ -829,6 +829,53 @@ object RenderBridge {
|
|
|
829
829
|
return state.result
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
+
fun measureHeight(
|
|
833
|
+
json: String,
|
|
834
|
+
themeJson: String?,
|
|
835
|
+
width: Float,
|
|
836
|
+
density: Float
|
|
837
|
+
): Float {
|
|
838
|
+
if (width <= 0) return 0f
|
|
839
|
+
|
|
840
|
+
val theme = EditorTheme.fromJson(themeJson)
|
|
841
|
+
val baseFontSize = theme?.text?.fontSize
|
|
842
|
+
?: theme?.paragraph?.fontSize
|
|
843
|
+
?: 16f
|
|
844
|
+
|
|
845
|
+
val spannable = buildSpannable(
|
|
846
|
+
json = json,
|
|
847
|
+
baseFontSize = baseFontSize,
|
|
848
|
+
textColor = android.graphics.Color.BLACK,
|
|
849
|
+
theme = theme,
|
|
850
|
+
density = density,
|
|
851
|
+
hostView = null
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
if (spannable.isEmpty()) return 0f
|
|
855
|
+
|
|
856
|
+
val contentInsets = theme?.contentInsets
|
|
857
|
+
val topInset = ((contentInsets?.top ?: 0f) * density).toInt()
|
|
858
|
+
val bottomInset = ((contentInsets?.bottom ?: 0f) * density).toInt()
|
|
859
|
+
val leftInset = ((contentInsets?.left ?: 0f) * density).toInt()
|
|
860
|
+
val rightInset = ((contentInsets?.right ?: 0f) * density).toInt()
|
|
861
|
+
|
|
862
|
+
val paint = android.text.TextPaint().apply {
|
|
863
|
+
textSize = baseFontSize * density
|
|
864
|
+
isAntiAlias = true
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
val availableWidth = (width - leftInset - rightInset).coerceAtLeast(0f).toInt()
|
|
868
|
+
|
|
869
|
+
val staticLayout = android.text.StaticLayout.Builder
|
|
870
|
+
.obtain(spannable, 0, spannable.length, paint, availableWidth)
|
|
871
|
+
.setAlignment(android.text.Layout.Alignment.ALIGN_NORMAL)
|
|
872
|
+
.setIncludePad(true)
|
|
873
|
+
.build()
|
|
874
|
+
|
|
875
|
+
val height = staticLayout.height + topInset + bottomInset
|
|
876
|
+
return height.toFloat()
|
|
877
|
+
}
|
|
878
|
+
|
|
832
879
|
private fun appendElements(
|
|
833
880
|
state: RenderBuildState,
|
|
834
881
|
elements: JSONArray,
|
|
@@ -1135,59 +1182,77 @@ object RenderBridge {
|
|
|
1135
1182
|
blockquoteDepth(blockStack) > 0
|
|
1136
1183
|
)
|
|
1137
1184
|
} ?: theme?.effectiveTextStyle("paragraph", inBlockquote = blockquoteDepth(blockStack) > 0)
|
|
1138
|
-
val resolvedTextSize = textStyle?.fontSize?.times(density) ?: baseFontSize
|
|
1139
|
-
val resolvedTextColor = textStyle?.color ?: textColor
|
|
1140
|
-
|
|
1141
|
-
// Apply base styling.
|
|
1142
|
-
builder.setSpan(
|
|
1143
|
-
ForegroundColorSpan(resolvedTextColor),
|
|
1144
|
-
start, end,
|
|
1145
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1146
|
-
)
|
|
1147
|
-
builder.setSpan(
|
|
1148
|
-
AbsoluteSizeSpan(resolvedTextSize.toInt(), false),
|
|
1149
|
-
start, end,
|
|
1150
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1151
|
-
)
|
|
1152
1185
|
|
|
1153
1186
|
// Determine which marks are active.
|
|
1154
|
-
var
|
|
1155
|
-
var
|
|
1156
|
-
var
|
|
1187
|
+
var markBold = false
|
|
1188
|
+
var markItalic = false
|
|
1189
|
+
var markUnderline = false
|
|
1157
1190
|
var hasStrike = false
|
|
1158
1191
|
var hasCode = false
|
|
1192
|
+
var isLink = false
|
|
1193
|
+
var linkHref: String? = null
|
|
1159
1194
|
for (mark in marks) {
|
|
1160
1195
|
when {
|
|
1161
1196
|
mark is String -> when (mark) {
|
|
1162
|
-
"bold", "strong" ->
|
|
1163
|
-
"italic", "em" ->
|
|
1164
|
-
"underline" ->
|
|
1197
|
+
"bold", "strong" -> markBold = true
|
|
1198
|
+
"italic", "em" -> markItalic = true
|
|
1199
|
+
"underline" -> markUnderline = true
|
|
1165
1200
|
"strike", "strikethrough" -> hasStrike = true
|
|
1166
1201
|
"code" -> hasCode = true
|
|
1167
1202
|
}
|
|
1168
1203
|
mark is JSONObject -> {
|
|
1169
1204
|
val markType = mark.optString("type", "")
|
|
1170
1205
|
if (markType == "link") {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
ForegroundColorSpan(LayoutConstants.DEFAULT_LINK_COLOR),
|
|
1174
|
-
start,
|
|
1175
|
-
end,
|
|
1176
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1177
|
-
)
|
|
1178
|
-
val href = mark.optString("href", "")
|
|
1179
|
-
if (href.isNotBlank()) {
|
|
1180
|
-
builder.setSpan(
|
|
1181
|
-
Annotation(NATIVE_LINK_HREF_ANNOTATION, href),
|
|
1182
|
-
start,
|
|
1183
|
-
end,
|
|
1184
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1185
|
-
)
|
|
1186
|
-
}
|
|
1206
|
+
isLink = true
|
|
1207
|
+
linkHref = mark.optString("href", "").takeIf { it.isNotBlank() }
|
|
1187
1208
|
}
|
|
1188
1209
|
}
|
|
1189
1210
|
}
|
|
1190
1211
|
}
|
|
1212
|
+
val linkTheme = if (isLink) theme?.links else null
|
|
1213
|
+
val effectiveTextStyle = textStyle?.mergedWith(linkTheme?.asTextStyle())
|
|
1214
|
+
?: linkTheme?.asTextStyle()
|
|
1215
|
+
val resolvedTextSize = effectiveTextStyle?.fontSize?.times(density) ?: baseFontSize
|
|
1216
|
+
val resolvedTextColor = if (isLink) {
|
|
1217
|
+
effectiveTextStyle?.color ?: LayoutConstants.DEFAULT_LINK_COLOR
|
|
1218
|
+
} else {
|
|
1219
|
+
effectiveTextStyle?.color ?: textColor
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Apply base styling.
|
|
1223
|
+
builder.setSpan(
|
|
1224
|
+
ForegroundColorSpan(resolvedTextColor),
|
|
1225
|
+
start, end,
|
|
1226
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1227
|
+
)
|
|
1228
|
+
builder.setSpan(
|
|
1229
|
+
AbsoluteSizeSpan(resolvedTextSize.toInt(), false),
|
|
1230
|
+
start, end,
|
|
1231
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1232
|
+
)
|
|
1233
|
+
linkTheme?.backgroundColor?.let { backgroundColor ->
|
|
1234
|
+
builder.setSpan(
|
|
1235
|
+
BackgroundColorSpan(backgroundColor),
|
|
1236
|
+
start,
|
|
1237
|
+
end,
|
|
1238
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1239
|
+
)
|
|
1240
|
+
}
|
|
1241
|
+
linkHref?.let { href ->
|
|
1242
|
+
builder.setSpan(
|
|
1243
|
+
Annotation(NATIVE_LINK_HREF_ANNOTATION, href),
|
|
1244
|
+
start,
|
|
1245
|
+
end,
|
|
1246
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1247
|
+
)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
val typefaceStyle = effectiveTextStyle?.typefaceStyle()
|
|
1251
|
+
val hasBold = markBold ||
|
|
1252
|
+
typefaceStyle?.let { it == Typeface.BOLD || it == Typeface.BOLD_ITALIC } == true
|
|
1253
|
+
val hasItalic = markItalic ||
|
|
1254
|
+
typefaceStyle?.let { it == Typeface.ITALIC || it == Typeface.BOLD_ITALIC } == true
|
|
1255
|
+
val hasUnderline = markUnderline || (isLink && (linkTheme?.underline ?: true))
|
|
1191
1256
|
|
|
1192
1257
|
// Apply bold/italic as a combined StyleSpan.
|
|
1193
1258
|
if (hasBold && hasItalic) {
|
|
@@ -1204,7 +1269,7 @@ object RenderBridge {
|
|
|
1204
1269
|
)
|
|
1205
1270
|
}
|
|
1206
1271
|
|
|
1207
|
-
val fontFamily =
|
|
1272
|
+
val fontFamily = effectiveTextStyle?.fontFamily
|
|
1208
1273
|
if (!hasCode && !fontFamily.isNullOrBlank()) {
|
|
1209
1274
|
builder.setSpan(
|
|
1210
1275
|
TypefaceSpan(fontFamily),
|
package/dist/EditorTheme.d.ts
CHANGED
|
@@ -26,6 +26,15 @@ export interface EditorTextStyle {
|
|
|
26
26
|
lineHeight?: number;
|
|
27
27
|
spacingAfter?: number;
|
|
28
28
|
}
|
|
29
|
+
export interface EditorLinkTheme {
|
|
30
|
+
fontFamily?: string;
|
|
31
|
+
fontSize?: number;
|
|
32
|
+
fontWeight?: EditorFontWeight;
|
|
33
|
+
fontStyle?: EditorFontStyle;
|
|
34
|
+
color?: string;
|
|
35
|
+
backgroundColor?: string;
|
|
36
|
+
underline?: boolean;
|
|
37
|
+
}
|
|
29
38
|
export interface EditorHeadingTheme {
|
|
30
39
|
h1?: EditorTextStyle;
|
|
31
40
|
h2?: EditorTextStyle;
|
|
@@ -85,6 +94,7 @@ export interface EditorTheme {
|
|
|
85
94
|
list?: EditorListTheme;
|
|
86
95
|
horizontalRule?: EditorHorizontalRuleTheme;
|
|
87
96
|
mentions?: EditorMentionTheme;
|
|
97
|
+
links?: EditorLinkTheme;
|
|
88
98
|
toolbar?: EditorToolbarTheme;
|
|
89
99
|
backgroundColor?: string;
|
|
90
100
|
borderRadius?: number;
|
|
@@ -14,6 +14,17 @@ export interface NativeProseViewerLinkPressEvent {
|
|
|
14
14
|
text: string;
|
|
15
15
|
}
|
|
16
16
|
type NativeProseViewerContent = DocumentJSON | string;
|
|
17
|
+
export type NativeProseViewerMentionPrefix = string | ((mention: NativeProseViewerMentionRenderContext) => string | null | undefined);
|
|
18
|
+
export interface NativeProseViewerMentionsAddonConfig {
|
|
19
|
+
trigger?: string;
|
|
20
|
+
prefix?: NativeProseViewerMentionPrefix;
|
|
21
|
+
theme?: EditorMentionTheme;
|
|
22
|
+
resolveTheme?: (mention: NativeProseViewerMentionRenderContext) => EditorMentionTheme | null | undefined;
|
|
23
|
+
onPress?: (event: NativeProseViewerMentionPressEvent) => void;
|
|
24
|
+
}
|
|
25
|
+
export interface NativeProseViewerAddons {
|
|
26
|
+
mentions?: NativeProseViewerMentionsAddonConfig;
|
|
27
|
+
}
|
|
17
28
|
interface NativeProseViewerBaseProps {
|
|
18
29
|
contentRevision?: string | number;
|
|
19
30
|
contentJSONRevision?: string | number;
|
|
@@ -23,10 +34,10 @@ interface NativeProseViewerBaseProps {
|
|
|
23
34
|
allowBase64Images?: boolean;
|
|
24
35
|
collapseTrailingEmptyParagraphs?: boolean;
|
|
25
36
|
enableLinkTaps?: boolean;
|
|
26
|
-
|
|
27
|
-
resolveMentionTheme?: (mention: NativeProseViewerMentionRenderContext) => EditorMentionTheme | null | undefined;
|
|
37
|
+
addons?: NativeProseViewerAddons;
|
|
28
38
|
onPressLink?: (event: NativeProseViewerLinkPressEvent) => void;
|
|
29
|
-
|
|
39
|
+
contentId?: string;
|
|
40
|
+
containerWidth?: number;
|
|
30
41
|
}
|
|
31
42
|
interface NativeProseViewerJsonProps extends NativeProseViewerBaseProps {
|
|
32
43
|
contentJSON: NativeProseViewerContent;
|
|
@@ -7,6 +7,7 @@ const expo_modules_core_1 = require("expo-modules-core");
|
|
|
7
7
|
const react_native_1 = require("react-native");
|
|
8
8
|
const addons_1 = require("./addons");
|
|
9
9
|
const EditorTheme_1 = require("./EditorTheme");
|
|
10
|
+
const heightCache_1 = require("./heightCache");
|
|
10
11
|
const schemas_1 = require("./schemas");
|
|
11
12
|
const NativeProseViewerView = (0, expo_modules_core_1.requireNativeViewManager)('NativeEditor', 'NativeProseViewer');
|
|
12
13
|
let nativeProseViewerModule = null;
|
|
@@ -62,18 +63,32 @@ function baseMentionLabelFromAttrs(attrs) {
|
|
|
62
63
|
const label = attrs.label;
|
|
63
64
|
return typeof label === 'string' && label.length > 0 ? label : 'mention';
|
|
64
65
|
}
|
|
65
|
-
function
|
|
66
|
-
const rawPrefix = typeof
|
|
66
|
+
function resolveConfiguredMentionPrefix(prefix, mention) {
|
|
67
|
+
const rawPrefix = typeof prefix === 'function' ? prefix(mention) : prefix;
|
|
67
68
|
return typeof rawPrefix === 'string' && rawPrefix.length > 0 ? rawPrefix : undefined;
|
|
68
69
|
}
|
|
70
|
+
function mentionTriggerFromAttrs(attrs) {
|
|
71
|
+
const trigger = attrs.mentionSuggestionChar;
|
|
72
|
+
return typeof trigger === 'string' && trigger.length > 0 ? trigger : undefined;
|
|
73
|
+
}
|
|
69
74
|
function applyMentionPrefix(label, prefix) {
|
|
70
75
|
if (!prefix || label.startsWith(prefix)) {
|
|
71
76
|
return label;
|
|
72
77
|
}
|
|
73
78
|
return `${prefix}${label}`;
|
|
74
79
|
}
|
|
75
|
-
function
|
|
80
|
+
function resolveMentionRenderedLabel(mentionContext, prefix, trigger) {
|
|
81
|
+
if (prefix !== undefined) {
|
|
82
|
+
return applyMentionPrefix(mentionContext.label, resolveConfiguredMentionPrefix(prefix, mentionContext));
|
|
83
|
+
}
|
|
84
|
+
return applyMentionPrefix(mentionContext.label, trigger ?? mentionTriggerFromAttrs(mentionContext.attrs));
|
|
85
|
+
}
|
|
86
|
+
function collectMentionPayloadsByDocPos(document, mentionsAddon) {
|
|
76
87
|
const mentions = new Map();
|
|
88
|
+
const effectiveMentionPrefix = mentionsAddon?.prefix;
|
|
89
|
+
const effectiveResolveMentionTheme = mentionsAddon?.resolveTheme;
|
|
90
|
+
const defaultMentionTheme = mentionsAddon?.theme;
|
|
91
|
+
const trigger = mentionsAddon?.trigger?.trim() || undefined;
|
|
77
92
|
const visit = (node, pos, isRoot = false) => {
|
|
78
93
|
if (node == null || typeof node !== 'object') {
|
|
79
94
|
return pos;
|
|
@@ -89,8 +104,14 @@ function collectMentionPayloadsByDocPos(document, mentionPrefix, resolveMentionT
|
|
|
89
104
|
const attrs = normalizeMentionAttrs(nodeRecord);
|
|
90
105
|
const label = baseMentionLabelFromAttrs(attrs);
|
|
91
106
|
const mentionContext = { docPos: pos, label, attrs };
|
|
92
|
-
const renderedLabel =
|
|
93
|
-
const
|
|
107
|
+
const renderedLabel = resolveMentionRenderedLabel(mentionContext, effectiveMentionPrefix, trigger);
|
|
108
|
+
const resolvedMentionTheme = effectiveResolveMentionTheme?.(mentionContext) ?? undefined;
|
|
109
|
+
const mentionTheme = defaultMentionTheme || resolvedMentionTheme
|
|
110
|
+
? {
|
|
111
|
+
...(defaultMentionTheme ?? {}),
|
|
112
|
+
...(resolvedMentionTheme ?? {}),
|
|
113
|
+
}
|
|
114
|
+
: undefined;
|
|
94
115
|
mentions.set(pos, {
|
|
95
116
|
...mentionContext,
|
|
96
117
|
renderedLabel,
|
|
@@ -332,7 +353,8 @@ function extractRenderError(json) {
|
|
|
332
353
|
}
|
|
333
354
|
}
|
|
334
355
|
function NativeProseViewer({ ...props }) {
|
|
335
|
-
const { contentRevision, contentJSONRevision, schema, theme, style, allowBase64Images = false, collapseTrailingEmptyParagraphs = true, enableLinkTaps = true,
|
|
356
|
+
const { contentRevision, contentJSONRevision, schema, theme, style, allowBase64Images = false, collapseTrailingEmptyParagraphs = true, enableLinkTaps = true, addons, onPressLink, contentId, containerWidth, } = props;
|
|
357
|
+
const mentionPressHandler = addons?.mentions?.onPress;
|
|
336
358
|
const contentJSON = 'contentJSON' in props ? props.contentJSON : undefined;
|
|
337
359
|
const contentHTML = 'contentHTML' in props ? props.contentHTML : undefined;
|
|
338
360
|
const resolvedContentRevision = contentRevision ?? contentJSONRevision;
|
|
@@ -349,7 +371,7 @@ function NativeProseViewer({ ...props }) {
|
|
|
349
371
|
const themeJson = (0, react_1.useMemo)(() => (0, EditorTheme_1.serializeEditorTheme)(theme), [theme]);
|
|
350
372
|
const mentionPayloadsByDocPos = (0, react_1.useMemo)(() => normalizedDocument == null
|
|
351
373
|
? new Map()
|
|
352
|
-
: collectMentionPayloadsByDocPos(normalizedDocument,
|
|
374
|
+
: collectMentionPayloadsByDocPos(normalizedDocument, addons?.mentions), [addons?.mentions, normalizedDocument]);
|
|
353
375
|
const renderJson = (0, react_1.useMemo)(() => {
|
|
354
376
|
const configJson = JSON.stringify({
|
|
355
377
|
schema: documentSchema,
|
|
@@ -382,6 +404,24 @@ function NativeProseViewer({ ...props }) {
|
|
|
382
404
|
const renderJsonIsCollapsedEmpty = (0, react_1.useMemo)(() => collapseTrailingEmptyParagraphs &&
|
|
383
405
|
renderElementsJsonContainsOnlyEmptyParagraphs(renderJson), [collapseTrailingEmptyParagraphs, renderJson]);
|
|
384
406
|
const [contentHeight, setContentHeight] = (0, react_1.useState)(null);
|
|
407
|
+
(0, react_1.useEffect)(() => {
|
|
408
|
+
setContentHeight(null);
|
|
409
|
+
}, [contentId]);
|
|
410
|
+
const renderJsonHash = (0, react_1.useMemo)(() => (0, heightCache_1.computeRenderJsonHash)(renderJson), [renderJson]);
|
|
411
|
+
const layoutContextKey = (0, react_1.useMemo)(() => containerWidth != null ? (0, heightCache_1.computeLayoutContextKey)(themeJson, containerWidth) : null, [themeJson, containerWidth]);
|
|
412
|
+
const preMeasuredHeight = (0, react_1.useMemo)(() => {
|
|
413
|
+
if (!contentId || layoutContextKey == null || containerWidth == null) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const cached = (0, heightCache_1.getHeightCache)(contentId, layoutContextKey, renderJsonHash);
|
|
417
|
+
if (cached != null)
|
|
418
|
+
return cached;
|
|
419
|
+
const measured = getNativeProseViewerModule().measureContentHeight(renderJson, themeJson, containerWidth);
|
|
420
|
+
if (measured > 0) {
|
|
421
|
+
(0, heightCache_1.setHeightCache)(contentId, layoutContextKey, renderJsonHash, measured);
|
|
422
|
+
}
|
|
423
|
+
return measured > 0 ? measured : null;
|
|
424
|
+
}, [contentId, containerWidth, renderJson, themeJson, layoutContextKey, renderJsonHash]);
|
|
385
425
|
const handleContentHeightChange = (0, react_1.useCallback)((event) => {
|
|
386
426
|
const density = react_native_1.Platform.OS === 'android' ? react_native_1.PixelRatio.get() : 1;
|
|
387
427
|
const nextHeight = Math.ceil(event.nativeEvent.contentHeight / density);
|
|
@@ -394,18 +434,21 @@ function NativeProseViewer({ ...props }) {
|
|
|
394
434
|
return;
|
|
395
435
|
}
|
|
396
436
|
setContentHeight((currentHeight) => currentHeight === nextHeight ? currentHeight : nextHeight);
|
|
397
|
-
|
|
437
|
+
if (contentId && layoutContextKey != null) {
|
|
438
|
+
(0, heightCache_1.setHeightCache)(contentId, layoutContextKey, renderJsonHash, nextHeight);
|
|
439
|
+
}
|
|
440
|
+
}, [renderJsonIsCollapsedEmpty, contentId, layoutContextKey, renderJsonHash]);
|
|
398
441
|
const handlePressMention = (0, react_1.useCallback)((event) => {
|
|
399
|
-
if (!
|
|
442
|
+
if (!mentionPressHandler)
|
|
400
443
|
return;
|
|
401
444
|
const { docPos, label } = event.nativeEvent;
|
|
402
445
|
const resolvedMention = mentionPayloadsByDocPos.get(docPos);
|
|
403
|
-
|
|
446
|
+
mentionPressHandler({
|
|
404
447
|
docPos,
|
|
405
448
|
label: resolvedMention?.renderedLabel ?? label,
|
|
406
449
|
attrs: resolvedMention?.attrs ?? {},
|
|
407
450
|
});
|
|
408
|
-
}, [mentionPayloadsByDocPos,
|
|
451
|
+
}, [mentionPayloadsByDocPos, mentionPressHandler]);
|
|
409
452
|
const handlePressLink = (0, react_1.useCallback)((event) => {
|
|
410
453
|
if (!onPressLink)
|
|
411
454
|
return;
|
|
@@ -422,11 +465,16 @@ function NativeProseViewer({ ...props }) {
|
|
|
422
465
|
else if (contentHeight != null && contentHeight > 0) {
|
|
423
466
|
measuredStyle = { minHeight: contentHeight };
|
|
424
467
|
}
|
|
468
|
+
else if (preMeasuredHeight != null && preMeasuredHeight > 0) {
|
|
469
|
+
measuredStyle = { minHeight: preMeasuredHeight };
|
|
470
|
+
}
|
|
425
471
|
return [
|
|
426
472
|
{ minHeight: renderJsonIsCollapsedEmpty ? 0 : 1 },
|
|
427
473
|
style,
|
|
428
474
|
measuredStyle,
|
|
429
475
|
];
|
|
430
|
-
}, [contentHeight, renderJsonIsCollapsedEmpty, style]);
|
|
431
|
-
return ((0, jsx_runtime_1.jsx)(NativeProseViewerView, { style: nativeStyle, renderJson: renderJson, themeJson: themeJson, collapsesWhenEmpty: collapseTrailingEmptyParagraphs, enableLinkTaps: enableLinkTaps, interceptLinkTaps: typeof onPressLink === 'function', onContentHeightChange: handleContentHeightChange, onPressLink: typeof onPressLink === 'function' ? handlePressLink : undefined, onPressMention: typeof
|
|
476
|
+
}, [contentHeight, preMeasuredHeight, renderJsonIsCollapsedEmpty, style]);
|
|
477
|
+
return ((0, jsx_runtime_1.jsx)(NativeProseViewerView, { style: nativeStyle, renderJson: renderJson, themeJson: themeJson, collapsesWhenEmpty: collapseTrailingEmptyParagraphs, enableLinkTaps: enableLinkTaps, interceptLinkTaps: typeof onPressLink === 'function', onContentHeightChange: handleContentHeightChange, onPressLink: typeof onPressLink === 'function' ? handlePressLink : undefined, onPressMention: typeof mentionPressHandler === 'function'
|
|
478
|
+
? handlePressMention
|
|
479
|
+
: undefined }));
|
|
432
480
|
}
|
|
@@ -957,6 +957,22 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
957
957
|
? { ...selectionEvent.attrs, ...resolvedAttrs }
|
|
958
958
|
: selectionEvent.attrs;
|
|
959
959
|
}, []);
|
|
960
|
+
const resolveMentionInsertionAttrs = (0, react_1.useCallback)((selectionEvent) => {
|
|
961
|
+
const attrs = resolveMentionSelectionAttrs(selectionEvent);
|
|
962
|
+
let resolvedTheme;
|
|
963
|
+
try {
|
|
964
|
+
resolvedTheme = addonsRef.current?.mentions?.resolveTheme?.({
|
|
965
|
+
...selectionEvent,
|
|
966
|
+
attrs,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
if (__DEV__) {
|
|
971
|
+
console.error('NativeRichTextEditor: mentions.resolveTheme threw', error);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return isRecord(resolvedTheme) ? { ...attrs, mentionTheme: resolvedTheme } : attrs;
|
|
975
|
+
}, [resolveMentionSelectionAttrs]);
|
|
960
976
|
const handleInlineMentionSuggestionPress = (0, react_1.useCallback)((suggestion) => {
|
|
961
977
|
const mentionQuery = mentionQueryEventRef.current;
|
|
962
978
|
const mentions = addonsRef.current?.mentions;
|
|
@@ -966,7 +982,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
966
982
|
bridgeRef.current.isDestroyed) {
|
|
967
983
|
return;
|
|
968
984
|
}
|
|
969
|
-
const attrs =
|
|
985
|
+
const attrs = resolveMentionInsertionAttrs({
|
|
970
986
|
trigger: mentionQuery.trigger,
|
|
971
987
|
suggestion,
|
|
972
988
|
attrs: resolveMentionSuggestionAttrs(suggestion, mentionQuery.trigger),
|
|
@@ -981,7 +997,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
981
997
|
attrs,
|
|
982
998
|
});
|
|
983
999
|
}
|
|
984
|
-
}, [
|
|
1000
|
+
}, [resolveMentionInsertionAttrs, runAndApply]);
|
|
985
1001
|
const handleAddonEvent = (0, react_1.useCallback)((event) => {
|
|
986
1002
|
let parsed = null;
|
|
987
1003
|
try {
|
|
@@ -1018,7 +1034,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1018
1034
|
attrs: parsed.attrs,
|
|
1019
1035
|
range: parsed.range,
|
|
1020
1036
|
};
|
|
1021
|
-
const finalAttrs =
|
|
1037
|
+
const finalAttrs = resolveMentionInsertionAttrs(selectionEvent);
|
|
1022
1038
|
const update = runAndApply(() => bridgeRef.current?.insertContentJsonAtSelectionScalar(parsed.range.anchor, parsed.range.head, (0, addons_1.buildMentionFragmentJson)(finalAttrs)) ?? null);
|
|
1023
1039
|
if (update) {
|
|
1024
1040
|
addonsRef.current?.mentions?.onSelect?.({
|
|
@@ -1039,7 +1055,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1039
1055
|
attrs: parsed.attrs,
|
|
1040
1056
|
});
|
|
1041
1057
|
}
|
|
1042
|
-
}, [
|
|
1058
|
+
}, [resolveMentionInsertionAttrs, runAndApply]);
|
|
1043
1059
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
1044
1060
|
focus() {
|
|
1045
1061
|
nativeViewRef.current?.focus?.();
|
package/dist/addons.d.ts
CHANGED
|
@@ -31,11 +31,13 @@ export interface MentionSelectionAttrsEvent {
|
|
|
31
31
|
head: number;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
export type MentionThemeResolveEvent = MentionSelectionAttrsEvent;
|
|
34
35
|
export interface MentionsAddonConfig {
|
|
35
36
|
trigger?: string;
|
|
36
37
|
suggestions?: readonly MentionSuggestion[];
|
|
37
38
|
theme?: EditorMentionTheme;
|
|
38
39
|
resolveSelectionAttrs?: (event: MentionSelectionAttrsEvent) => Record<string, unknown> | null | undefined;
|
|
40
|
+
resolveTheme?: (event: MentionThemeResolveEvent) => EditorMentionTheme | null | undefined;
|
|
39
41
|
onQueryChange?: (event: MentionQueryChangeEvent) => void;
|
|
40
42
|
onSelect?: (event: MentionSelectEvent) => void;
|
|
41
43
|
}
|
|
@@ -53,6 +55,7 @@ export interface SerializedMentionsAddonConfig {
|
|
|
53
55
|
trigger: string;
|
|
54
56
|
theme?: EditorMentionTheme;
|
|
55
57
|
resolveSelectionAttrs?: boolean;
|
|
58
|
+
resolveTheme?: boolean;
|
|
56
59
|
suggestions: SerializedMentionSuggestion[];
|
|
57
60
|
}
|
|
58
61
|
export interface SerializedEditorAddons {
|
package/dist/addons.js
CHANGED
|
@@ -57,6 +57,7 @@ function normalizeEditorAddons(addons) {
|
|
|
57
57
|
...(typeof addons.mentions.resolveSelectionAttrs === 'function'
|
|
58
58
|
? { resolveSelectionAttrs: true }
|
|
59
59
|
: {}),
|
|
60
|
+
...(typeof addons.mentions.resolveTheme === 'function' ? { resolveTheme: true } : {}),
|
|
60
61
|
suggestions,
|
|
61
62
|
},
|
|
62
63
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const MAX_CACHE_SIZE = 500;
|
|
2
|
+
export declare function computeRenderJsonHash(input: string): number;
|
|
3
|
+
export declare function computeLayoutContextKey(themeJson: string | undefined, containerWidth: number): string;
|
|
4
|
+
export declare function getHeightCache(contentId: string, layoutContextKey: string, renderJsonHash: number): number | null;
|
|
5
|
+
export declare function setHeightCache(contentId: string, layoutContextKey: string, renderJsonHash: number, height: number): void;
|
|
6
|
+
export declare function clearHeightCache(): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_CACHE_SIZE = void 0;
|
|
4
|
+
exports.computeRenderJsonHash = computeRenderJsonHash;
|
|
5
|
+
exports.computeLayoutContextKey = computeLayoutContextKey;
|
|
6
|
+
exports.getHeightCache = getHeightCache;
|
|
7
|
+
exports.setHeightCache = setHeightCache;
|
|
8
|
+
exports.clearHeightCache = clearHeightCache;
|
|
9
|
+
exports.MAX_CACHE_SIZE = 500;
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
function computeRenderJsonHash(input) {
|
|
12
|
+
let hash = 5381;
|
|
13
|
+
for (let i = 0; i < input.length; i++) {
|
|
14
|
+
hash = ((hash << 5) + hash + input.charCodeAt(i)) | 0;
|
|
15
|
+
}
|
|
16
|
+
return hash >>> 0;
|
|
17
|
+
}
|
|
18
|
+
function computeLayoutContextKey(themeJson, containerWidth) {
|
|
19
|
+
return `${themeJson ?? ''}\x00${containerWidth}`;
|
|
20
|
+
}
|
|
21
|
+
function getHeightCache(contentId, layoutContextKey, renderJsonHash) {
|
|
22
|
+
const entry = cache.get(contentId);
|
|
23
|
+
if (!entry)
|
|
24
|
+
return null;
|
|
25
|
+
if (entry.layoutContextKey !== layoutContextKey ||
|
|
26
|
+
entry.renderJsonHash !== renderJsonHash) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
cache.delete(contentId);
|
|
30
|
+
cache.set(contentId, entry);
|
|
31
|
+
return entry.height;
|
|
32
|
+
}
|
|
33
|
+
function setHeightCache(contentId, layoutContextKey, renderJsonHash, height) {
|
|
34
|
+
cache.delete(contentId);
|
|
35
|
+
if (cache.size >= exports.MAX_CACHE_SIZE) {
|
|
36
|
+
const oldestKey = cache.keys().next().value;
|
|
37
|
+
if (oldestKey !== undefined) {
|
|
38
|
+
cache.delete(oldestKey);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
cache.set(contentId, { height, renderJsonHash, layoutContextKey });
|
|
42
|
+
}
|
|
43
|
+
function clearHeightCache() {
|
|
44
|
+
cache.clear();
|
|
45
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
|
|
2
|
-
export { NativeProseViewer, type NativeProseViewerProps, type NativeProseViewerLinkPressEvent, type NativeProseViewerMentionRenderContext, type NativeProseViewerMentionPressEvent, } from './NativeProseViewer';
|
|
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
|
-
export type { EditorContentInsets, EditorTheme, EditorTextStyle, EditorHeadingTheme, EditorListTheme, EditorHorizontalRuleTheme, EditorMentionTheme, EditorToolbarTheme, EditorToolbarAppearance, EditorFontStyle, EditorFontWeight, } from './EditorTheme';
|
|
5
|
-
export { MENTION_NODE_NAME, mentionNodeSpec, withMentionsSchema, buildMentionFragmentJson, type EditorAddons, type MentionsAddonConfig, type MentionSuggestion, type MentionQueryChangeEvent, type MentionSelectionAttrsEvent, type MentionSelectEvent, type EditorAddonEvent, } from './addons';
|
|
4
|
+
export type { EditorContentInsets, EditorTheme, EditorTextStyle, EditorLinkTheme, EditorHeadingTheme, EditorListTheme, EditorHorizontalRuleTheme, EditorMentionTheme, EditorToolbarTheme, EditorToolbarAppearance, EditorFontStyle, EditorFontWeight, } from './EditorTheme';
|
|
5
|
+
export { MENTION_NODE_NAME, mentionNodeSpec, withMentionsSchema, buildMentionFragmentJson, type EditorAddons, type MentionsAddonConfig, type MentionSuggestion, type MentionQueryChangeEvent, type MentionSelectionAttrsEvent, type MentionThemeResolveEvent, type MentionSelectEvent, type EditorAddonEvent, } from './addons';
|
|
6
6
|
export { tiptapSchema, prosemirrorSchema, IMAGE_NODE_NAME, imageNodeSpec, withImagesSchema, buildImageFragmentJson, type SchemaDefinition, type NodeSpec, type MarkSpec, type AttrSpec, type ImageNodeAttributes, } from './schemas';
|
|
7
7
|
export { createYjsCollaborationController, useYjsCollaboration, type YjsCollaborationOptions, type YjsCollaborationState, type YjsTransportStatus, type LocalAwarenessState, type LocalAwarenessUser, type UseYjsCollaborationResult, type YjsCollaborationController, } from './YjsCollaboration';
|
|
8
8
|
export type { Selection, ActiveState, HistoryState, EditorUpdate, DocumentJSON, CollaborationPeer, EncodedCollaborationStateInput, } from './NativeEditorBridge';
|
|
9
9
|
export { encodeCollaborationStateBase64, decodeCollaborationStateBase64, } from './NativeEditorBridge';
|
|
10
|
+
export { clearHeightCache } from './heightCache';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.decodeCollaborationStateBase64 = exports.encodeCollaborationStateBase64 = exports.useYjsCollaboration = exports.createYjsCollaborationController = exports.buildImageFragmentJson = exports.withImagesSchema = exports.imageNodeSpec = exports.IMAGE_NODE_NAME = exports.prosemirrorSchema = exports.tiptapSchema = exports.buildMentionFragmentJson = exports.withMentionsSchema = exports.mentionNodeSpec = exports.MENTION_NODE_NAME = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = exports.EditorToolbar = exports.NativeProseViewer = exports.NativeRichTextEditor = void 0;
|
|
3
|
+
exports.clearHeightCache = exports.decodeCollaborationStateBase64 = exports.encodeCollaborationStateBase64 = exports.useYjsCollaboration = exports.createYjsCollaborationController = exports.buildImageFragmentJson = exports.withImagesSchema = exports.imageNodeSpec = exports.IMAGE_NODE_NAME = exports.prosemirrorSchema = exports.tiptapSchema = exports.buildMentionFragmentJson = exports.withMentionsSchema = exports.mentionNodeSpec = exports.MENTION_NODE_NAME = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = exports.EditorToolbar = exports.NativeProseViewer = exports.NativeRichTextEditor = void 0;
|
|
4
4
|
var NativeRichTextEditor_1 = require("./NativeRichTextEditor");
|
|
5
5
|
Object.defineProperty(exports, "NativeRichTextEditor", { enumerable: true, get: function () { return NativeRichTextEditor_1.NativeRichTextEditor; } });
|
|
6
6
|
var NativeProseViewer_1 = require("./NativeProseViewer");
|
|
@@ -26,3 +26,5 @@ Object.defineProperty(exports, "useYjsCollaboration", { enumerable: true, get: f
|
|
|
26
26
|
var NativeEditorBridge_1 = require("./NativeEditorBridge");
|
|
27
27
|
Object.defineProperty(exports, "encodeCollaborationStateBase64", { enumerable: true, get: function () { return NativeEditorBridge_1.encodeCollaborationStateBase64; } });
|
|
28
28
|
Object.defineProperty(exports, "decodeCollaborationStateBase64", { enumerable: true, get: function () { return NativeEditorBridge_1.decodeCollaborationStateBase64; } });
|
|
29
|
+
var heightCache_1 = require("./heightCache");
|
|
30
|
+
Object.defineProperty(exports, "clearHeightCache", { enumerable: true, get: function () { return heightCache_1.clearHeightCache; } });
|
package/ios/EditorAddons.swift
CHANGED
|
@@ -28,12 +28,14 @@ struct NativeMentionsAddonConfig {
|
|
|
28
28
|
let suggestions: [NativeMentionSuggestion]
|
|
29
29
|
let theme: EditorMentionTheme?
|
|
30
30
|
let resolveSelectionAttrs: Bool
|
|
31
|
+
let resolveTheme: Bool
|
|
31
32
|
|
|
32
33
|
init?(dictionary: [String: Any]) {
|
|
33
34
|
let trigger = (dictionary["trigger"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
34
35
|
self.trigger = (trigger?.isEmpty == false ? trigger : "@") ?? "@"
|
|
35
36
|
self.suggestions = ((dictionary["suggestions"] as? [[String: Any]]) ?? []).compactMap(NativeMentionSuggestion.init(dictionary:))
|
|
36
37
|
self.resolveSelectionAttrs = dictionary["resolveSelectionAttrs"] as? Bool ?? false
|
|
38
|
+
self.resolveTheme = dictionary["resolveTheme"] as? Bool ?? false
|
|
37
39
|
if let theme = dictionary["theme"] as? [String: Any] {
|
|
38
40
|
self.theme = EditorMentionTheme(dictionary: theme)
|
|
39
41
|
} else {
|
|
Binary file
|
|
Binary file
|
package/ios/EditorTheme.swift
CHANGED
|
@@ -124,6 +124,35 @@ struct EditorBlockquoteTheme {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
struct EditorLinkTheme {
|
|
128
|
+
var fontFamily: String?
|
|
129
|
+
var fontSize: CGFloat?
|
|
130
|
+
var fontWeight: String?
|
|
131
|
+
var fontStyle: String?
|
|
132
|
+
var color: UIColor?
|
|
133
|
+
var backgroundColor: UIColor?
|
|
134
|
+
var underline: Bool?
|
|
135
|
+
|
|
136
|
+
init(dictionary: [String: Any]) {
|
|
137
|
+
fontFamily = dictionary["fontFamily"] as? String
|
|
138
|
+
fontSize = EditorTheme.cgFloat(dictionary["fontSize"])
|
|
139
|
+
fontWeight = dictionary["fontWeight"] as? String
|
|
140
|
+
fontStyle = dictionary["fontStyle"] as? String
|
|
141
|
+
color = EditorTheme.color(from: dictionary["color"])
|
|
142
|
+
backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
|
|
143
|
+
underline = dictionary["underline"] as? Bool
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func resolvedFont(fallback: UIFont) -> UIFont {
|
|
147
|
+
EditorTextStyle(
|
|
148
|
+
fontFamily: fontFamily,
|
|
149
|
+
fontSize: fontSize,
|
|
150
|
+
fontWeight: fontWeight,
|
|
151
|
+
fontStyle: fontStyle
|
|
152
|
+
).resolvedFont(fallback: fallback)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
struct EditorMentionTheme {
|
|
128
157
|
var textColor: UIColor?
|
|
129
158
|
var backgroundColor: UIColor?
|
|
@@ -268,6 +297,7 @@ struct EditorTheme {
|
|
|
268
297
|
var list: EditorListTheme?
|
|
269
298
|
var horizontalRule: EditorHorizontalRuleTheme?
|
|
270
299
|
var mentions: EditorMentionTheme?
|
|
300
|
+
var links: EditorLinkTheme?
|
|
271
301
|
var toolbar: EditorToolbarTheme?
|
|
272
302
|
var backgroundColor: UIColor?
|
|
273
303
|
var borderRadius: CGFloat?
|
|
@@ -309,6 +339,9 @@ struct EditorTheme {
|
|
|
309
339
|
if let mentions = dictionary["mentions"] as? [String: Any] {
|
|
310
340
|
self.mentions = EditorMentionTheme(dictionary: mentions)
|
|
311
341
|
}
|
|
342
|
+
if let links = dictionary["links"] as? [String: Any] {
|
|
343
|
+
self.links = EditorLinkTheme(dictionary: links)
|
|
344
|
+
}
|
|
312
345
|
if let toolbar = dictionary["toolbar"] as? [String: Any] {
|
|
313
346
|
self.toolbar = EditorToolbarTheme(dictionary: toolbar)
|
|
314
347
|
}
|
|
@@ -2218,7 +2218,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2218
2218
|
}
|
|
2219
2219
|
|
|
2220
2220
|
let attrs = resolvedMentionAttrs(trigger: mentions.trigger, suggestion: suggestion)
|
|
2221
|
-
if mentions.resolveSelectionAttrs {
|
|
2221
|
+
if mentions.resolveSelectionAttrs || mentions.resolveTheme {
|
|
2222
2222
|
emitMentionSelectRequest(
|
|
2223
2223
|
trigger: mentions.trigger,
|
|
2224
2224
|
suggestion: suggestion,
|
|
@@ -268,6 +268,14 @@ public class NativeEditorModule: Module {
|
|
|
268
268
|
}
|
|
269
269
|
return editorSetJson(id: editorId, json: json)
|
|
270
270
|
}
|
|
271
|
+
Function("measureContentHeight") { (renderJson: String, themeJson: String?, width: Double) -> Double in
|
|
272
|
+
let height = RenderBridge.measureHeight(
|
|
273
|
+
forRenderJSON: renderJson,
|
|
274
|
+
themeJSON: themeJson,
|
|
275
|
+
width: CGFloat(width)
|
|
276
|
+
)
|
|
277
|
+
return Double(height)
|
|
278
|
+
}
|
|
271
279
|
Function("renderDocumentHtml") { (configJson: String, html: String) -> String in
|
|
272
280
|
let editorId = editorCreate(configJson: configJson)
|
|
273
281
|
defer {
|
package/ios/RenderBridge.swift
CHANGED
|
@@ -243,7 +243,8 @@ final class RenderBridge {
|
|
|
243
243
|
let baseAttrs = attributesForMarks(
|
|
244
244
|
marks,
|
|
245
245
|
baseFont: blockFont,
|
|
246
|
-
textColor: blockColor
|
|
246
|
+
textColor: blockColor,
|
|
247
|
+
theme: theme
|
|
247
248
|
)
|
|
248
249
|
let attrs = applyBlockStyle(
|
|
249
250
|
to: baseAttrs,
|
|
@@ -541,6 +542,63 @@ final class RenderBridge {
|
|
|
541
542
|
return result
|
|
542
543
|
}
|
|
543
544
|
|
|
545
|
+
// MARK: - Height Pre-Measurement
|
|
546
|
+
|
|
547
|
+
static func measureHeight(
|
|
548
|
+
forRenderJSON renderJSON: String,
|
|
549
|
+
themeJSON: String?,
|
|
550
|
+
width: CGFloat
|
|
551
|
+
) -> CGFloat {
|
|
552
|
+
guard width > 0 else { return 0 }
|
|
553
|
+
|
|
554
|
+
let theme = EditorTheme.from(json: themeJSON)
|
|
555
|
+
let baseFontSize = theme?.text?.fontSize ?? theme?.paragraph?.fontSize ?? 16
|
|
556
|
+
let baseFont = UIFont.systemFont(ofSize: baseFontSize)
|
|
557
|
+
let textColor = theme?.text?.color ?? UIColor.label
|
|
558
|
+
|
|
559
|
+
let attributedString = renderElements(
|
|
560
|
+
fromJSON: renderJSON,
|
|
561
|
+
baseFont: baseFont,
|
|
562
|
+
textColor: textColor,
|
|
563
|
+
theme: theme
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
guard attributedString.length > 0 else { return 0 }
|
|
567
|
+
|
|
568
|
+
let contentInsets = theme?.contentInsets
|
|
569
|
+
let topInset = contentInsets?.top ?? 0
|
|
570
|
+
let bottomInset = contentInsets?.bottom ?? 0
|
|
571
|
+
let leftInset = contentInsets?.left ?? 0
|
|
572
|
+
let rightInset = contentInsets?.right ?? 0
|
|
573
|
+
|
|
574
|
+
// When contentInsets are set, lineFragmentPadding is 0 (matches
|
|
575
|
+
// RichTextEditorView.theme didSet). Otherwise use the UITextView
|
|
576
|
+
// default of 5.
|
|
577
|
+
let lineFragmentPadding: CGFloat = contentInsets != nil ? 0 : 5
|
|
578
|
+
|
|
579
|
+
let textStorage = NSTextStorage(attributedString: attributedString)
|
|
580
|
+
let layoutManager = NSLayoutManager()
|
|
581
|
+
let containerWidth = width - leftInset - rightInset - lineFragmentPadding * 2
|
|
582
|
+
let textContainer = NSTextContainer(
|
|
583
|
+
size: CGSize(width: max(containerWidth, 0), height: .greatestFiniteMagnitude)
|
|
584
|
+
)
|
|
585
|
+
textContainer.lineFragmentPadding = 0
|
|
586
|
+
|
|
587
|
+
layoutManager.addTextContainer(textContainer)
|
|
588
|
+
textStorage.addLayoutManager(layoutManager)
|
|
589
|
+
|
|
590
|
+
layoutManager.ensureLayout(for: textContainer)
|
|
591
|
+
|
|
592
|
+
var usedRect = layoutManager.usedRect(for: textContainer)
|
|
593
|
+
let extraLineFragmentRect = layoutManager.extraLineFragmentRect
|
|
594
|
+
if !extraLineFragmentRect.isEmpty {
|
|
595
|
+
usedRect = usedRect.union(extraLineFragmentRect)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let height = ceil(usedRect.height + topInset + bottomInset)
|
|
599
|
+
return height
|
|
600
|
+
}
|
|
601
|
+
|
|
544
602
|
// MARK: - Mark Handling
|
|
545
603
|
|
|
546
604
|
/// Build NSAttributedString attributes for a set of render marks.
|
|
@@ -556,7 +614,8 @@ final class RenderBridge {
|
|
|
556
614
|
static func attributesForMarks(
|
|
557
615
|
_ marks: [Any],
|
|
558
616
|
baseFont: UIFont,
|
|
559
|
-
textColor: UIColor
|
|
617
|
+
textColor: UIColor,
|
|
618
|
+
theme: EditorTheme? = nil
|
|
560
619
|
) -> [NSAttributedString.Key: Any] {
|
|
561
620
|
var attrs = defaultAttributes(baseFont: baseFont, textColor: textColor)
|
|
562
621
|
|
|
@@ -566,6 +625,8 @@ final class RenderBridge {
|
|
|
566
625
|
|
|
567
626
|
var traits: UIFontDescriptor.SymbolicTraits = []
|
|
568
627
|
var useMonospace = false
|
|
628
|
+
var linkTheme: EditorLinkTheme?
|
|
629
|
+
var shouldUnderline = false
|
|
569
630
|
for mark in marks {
|
|
570
631
|
let markObject = mark as? [String: Any]
|
|
571
632
|
let markType: String
|
|
@@ -583,14 +644,20 @@ final class RenderBridge {
|
|
|
583
644
|
case "italic", "em":
|
|
584
645
|
traits.insert(.traitItalic)
|
|
585
646
|
case "underline":
|
|
586
|
-
|
|
647
|
+
shouldUnderline = true
|
|
587
648
|
case "strike", "strikethrough":
|
|
588
649
|
attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
589
650
|
case "code":
|
|
590
651
|
useMonospace = true
|
|
591
652
|
case "link":
|
|
592
|
-
|
|
593
|
-
|
|
653
|
+
linkTheme = theme?.links
|
|
654
|
+
if theme?.links?.underline ?? true {
|
|
655
|
+
shouldUnderline = true
|
|
656
|
+
}
|
|
657
|
+
attrs[.foregroundColor] = theme?.links?.color ?? UIColor.systemBlue
|
|
658
|
+
if let backgroundColor = theme?.links?.backgroundColor {
|
|
659
|
+
attrs[.backgroundColor] = backgroundColor
|
|
660
|
+
}
|
|
594
661
|
if let href = markObject?["href"] as? String, !href.isEmpty {
|
|
595
662
|
attrs[RenderBridgeAttributes.linkHref] = href
|
|
596
663
|
}
|
|
@@ -599,11 +666,11 @@ final class RenderBridge {
|
|
|
599
666
|
}
|
|
600
667
|
}
|
|
601
668
|
|
|
602
|
-
var resolvedFont = baseFont
|
|
669
|
+
var resolvedFont = linkTheme?.resolvedFont(fallback: baseFont) ?? baseFont
|
|
603
670
|
|
|
604
671
|
if useMonospace {
|
|
605
672
|
resolvedFont = UIFont.monospacedSystemFont(
|
|
606
|
-
ofSize:
|
|
673
|
+
ofSize: resolvedFont.pointSize,
|
|
607
674
|
weight: traits.contains(.traitBold) ? .bold : .regular
|
|
608
675
|
)
|
|
609
676
|
// Monospaced doesn't support italic via descriptor traits, but we
|
|
@@ -612,15 +679,19 @@ final class RenderBridge {
|
|
|
612
679
|
if traits.contains(.traitItalic) && !traits.contains(.traitBold) {
|
|
613
680
|
// For code+italic only, try applying italic trait.
|
|
614
681
|
if let descriptor = resolvedFont.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
|
615
|
-
resolvedFont = UIFont(descriptor: descriptor, size:
|
|
682
|
+
resolvedFont = UIFont(descriptor: descriptor, size: resolvedFont.pointSize)
|
|
616
683
|
}
|
|
617
684
|
}
|
|
618
685
|
} else if !traits.isEmpty {
|
|
619
|
-
|
|
620
|
-
|
|
686
|
+
let mergedTraits = resolvedFont.fontDescriptor.symbolicTraits.union(traits)
|
|
687
|
+
if let descriptor = resolvedFont.fontDescriptor.withSymbolicTraits(mergedTraits) {
|
|
688
|
+
resolvedFont = UIFont(descriptor: descriptor, size: resolvedFont.pointSize)
|
|
621
689
|
}
|
|
622
690
|
}
|
|
623
691
|
|
|
692
|
+
if shouldUnderline {
|
|
693
|
+
attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
694
|
+
}
|
|
624
695
|
attrs[.font] = resolvedFont
|
|
625
696
|
return attrs
|
|
626
697
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollohg/react-native-prose-editor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Native rich text editor with Rust core for React Native",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/apollohg/react-native-prose-editor",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|