@apollohg/react-native-prose-editor 0.5.3 → 0.5.5
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/EditorTheme.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +5 -0
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +144 -23
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +25 -3
- package/dist/EditorTheme.d.ts +3 -0
- package/dist/EditorToolbar.js +11 -6
- package/dist/NativeProseViewer.js +82 -23
- package/dist/NativeRichTextEditor.js +189 -30
- 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 +6 -0
- package/ios/NativeEditorModule.swift +4 -0
- package/ios/NativeProseViewerExpoView.swift +94 -15
- package/ios/RenderBridge.swift +5 -0
- 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
|
@@ -55,6 +55,7 @@ data class EditorTextStyle(
|
|
|
55
55
|
|
|
56
56
|
data class EditorListTheme(
|
|
57
57
|
val indent: Float? = null,
|
|
58
|
+
val baseIndentMultiplier: Float? = null,
|
|
58
59
|
val itemSpacing: Float? = null,
|
|
59
60
|
val markerColor: Int? = null,
|
|
60
61
|
val markerScale: Float? = null
|
|
@@ -64,6 +65,7 @@ data class EditorListTheme(
|
|
|
64
65
|
json ?: return null
|
|
65
66
|
return EditorListTheme(
|
|
66
67
|
indent = json.optNullableFloat("indent"),
|
|
68
|
+
baseIndentMultiplier = json.optNullableFloat("baseIndentMultiplier"),
|
|
67
69
|
itemSpacing = json.optNullableFloat("itemSpacing"),
|
|
68
70
|
markerColor = parseColor(json.optNullableString("markerColor")),
|
|
69
71
|
markerScale = json.optNullableFloat("markerScale")
|
|
@@ -194,6 +196,8 @@ data class EditorToolbarTheme(
|
|
|
194
196
|
val borderColor: Int? = null,
|
|
195
197
|
val borderWidth: Float? = null,
|
|
196
198
|
val borderRadius: Float? = null,
|
|
199
|
+
val marginTop: Float? = null,
|
|
200
|
+
val showTopBorder: Boolean? = null,
|
|
197
201
|
val keyboardOffset: Float? = null,
|
|
198
202
|
val horizontalInset: Float? = null,
|
|
199
203
|
val separatorColor: Int? = null,
|
|
@@ -222,6 +226,8 @@ data class EditorToolbarTheme(
|
|
|
222
226
|
borderColor = parseColor(json.optNullableString("borderColor")),
|
|
223
227
|
borderWidth = json.optNullableFloat("borderWidth"),
|
|
224
228
|
borderRadius = json.optNullableFloat("borderRadius"),
|
|
229
|
+
marginTop = json.optNullableFloat("marginTop"),
|
|
230
|
+
showTopBorder = if (json.has("showTopBorder")) json.optBoolean("showTopBorder") else null,
|
|
225
231
|
keyboardOffset = json.optNullableFloat("keyboardOffset"),
|
|
226
232
|
horizontalInset = json.optNullableFloat("horizontalInset"),
|
|
227
233
|
separatorColor = parseColor(json.optNullableString("separatorColor")),
|
|
@@ -386,6 +386,11 @@ class NativeEditorModule : Module() {
|
|
|
386
386
|
Prop("themeJson") { view: NativeProseViewerExpoView, themeJson: String? ->
|
|
387
387
|
view.setThemeJson(themeJson)
|
|
388
388
|
}
|
|
389
|
+
Prop("collapsesWhenEmpty") {
|
|
390
|
+
view: NativeProseViewerExpoView,
|
|
391
|
+
collapsesWhenEmpty: Boolean? ->
|
|
392
|
+
view.setCollapsesWhenEmpty(collapsesWhenEmpty)
|
|
393
|
+
}
|
|
389
394
|
Prop("enableLinkTaps") { view: NativeProseViewerExpoView, enableLinkTaps: Boolean? ->
|
|
390
395
|
view.setEnableLinkTaps(enableLinkTaps)
|
|
391
396
|
}
|
|
@@ -10,6 +10,8 @@ import android.view.ViewGroup
|
|
|
10
10
|
import expo.modules.kotlin.AppContext
|
|
11
11
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
12
12
|
import expo.modules.kotlin.views.ExpoView
|
|
13
|
+
import kotlin.math.abs
|
|
14
|
+
import org.json.JSONArray
|
|
13
15
|
|
|
14
16
|
class NativeProseViewerExpoView(
|
|
15
17
|
context: Context,
|
|
@@ -26,8 +28,11 @@ class NativeProseViewerExpoView(
|
|
|
26
28
|
private var lastRenderJson: String? = null
|
|
27
29
|
private var lastThemeJson: String? = null
|
|
28
30
|
private var lastEmittedContentHeight = 0
|
|
31
|
+
private var collapsesWhenEmpty = true
|
|
32
|
+
private var isCollapsedEmptyContent = false
|
|
29
33
|
private var enableLinkTaps = true
|
|
30
34
|
private var interceptLinkTaps = false
|
|
35
|
+
internal var suppressContentHeightEventsForTesting = false
|
|
31
36
|
|
|
32
37
|
init {
|
|
33
38
|
proseView.setBaseStyle(
|
|
@@ -36,6 +41,9 @@ class NativeProseViewerExpoView(
|
|
|
36
41
|
Color.TRANSPARENT
|
|
37
42
|
)
|
|
38
43
|
proseView.isEditable = false
|
|
44
|
+
proseView.inputType = android.text.InputType.TYPE_CLASS_TEXT or
|
|
45
|
+
android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE or
|
|
46
|
+
android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
|
39
47
|
proseView.setImageResizingEnabled(false)
|
|
40
48
|
proseView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
41
49
|
proseView.isFocusable = false
|
|
@@ -84,22 +92,25 @@ class NativeProseViewerExpoView(
|
|
|
84
92
|
fun setRenderJson(renderJson: String?) {
|
|
85
93
|
if (lastRenderJson == renderJson) return
|
|
86
94
|
lastRenderJson = renderJson
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
requestLayout()
|
|
90
|
-
emitContentHeightIfNeeded(force = true)
|
|
91
|
-
}
|
|
95
|
+
applyRenderJson()
|
|
96
|
+
requestLayout()
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
fun setThemeJson(themeJson: String?) {
|
|
95
100
|
if (lastThemeJson == themeJson) return
|
|
96
101
|
lastThemeJson = themeJson
|
|
97
102
|
proseView.applyTheme(EditorTheme.fromJson(themeJson))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
applyRenderJson()
|
|
104
|
+
requestLayout()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fun setCollapsesWhenEmpty(collapsesWhenEmpty: Boolean?) {
|
|
108
|
+
val nextValue = collapsesWhenEmpty ?: true
|
|
109
|
+
if (this.collapsesWhenEmpty == nextValue) return
|
|
110
|
+
this.collapsesWhenEmpty = nextValue
|
|
111
|
+
updateCollapsedEmptyState()
|
|
112
|
+
requestLayout()
|
|
113
|
+
emitContentHeightIfNeeded(force = true)
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
fun setEnableLinkTaps(enableLinkTaps: Boolean?) {
|
|
@@ -111,6 +122,12 @@ class NativeProseViewerExpoView(
|
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
125
|
+
if (isCollapsedEmptyContent) {
|
|
126
|
+
setMeasuredDimension(resolveSize(0, widthMeasureSpec), 0)
|
|
127
|
+
emitContentHeightIfNeeded()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
val childWidthSpec = getChildMeasureSpec(
|
|
115
132
|
widthMeasureSpec,
|
|
116
133
|
paddingLeft + paddingRight,
|
|
@@ -122,15 +139,29 @@ class NativeProseViewerExpoView(
|
|
|
122
139
|
)
|
|
123
140
|
proseView.measure(childWidthSpec, childHeightSpec)
|
|
124
141
|
|
|
142
|
+
val resolvedContentHeight = proseView.resolveAutoGrowHeight()
|
|
125
143
|
val desiredWidth = proseView.measuredWidth + paddingLeft + paddingRight
|
|
126
|
-
val desiredHeight =
|
|
144
|
+
val desiredHeight = resolvedContentHeight + paddingTop + paddingBottom
|
|
145
|
+
val measuredHeight = when (View.MeasureSpec.getMode(heightMeasureSpec)) {
|
|
146
|
+
View.MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(
|
|
147
|
+
View.MeasureSpec.getSize(heightMeasureSpec)
|
|
148
|
+
)
|
|
149
|
+
else -> desiredHeight
|
|
150
|
+
}
|
|
127
151
|
setMeasuredDimension(
|
|
128
152
|
resolveSize(desiredWidth, widthMeasureSpec),
|
|
129
|
-
|
|
153
|
+
measuredHeight
|
|
130
154
|
)
|
|
155
|
+
emitContentHeightIfNeeded(measuredContentHeight = desiredHeight)
|
|
131
156
|
}
|
|
132
157
|
|
|
133
158
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
159
|
+
if (isCollapsedEmptyContent) {
|
|
160
|
+
proseView.layout(paddingLeft, paddingTop, right - left - paddingRight, paddingTop)
|
|
161
|
+
emitContentHeightIfNeeded()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
134
165
|
val childLeft = paddingLeft
|
|
135
166
|
val childTop = paddingTop
|
|
136
167
|
proseView.layout(
|
|
@@ -142,30 +173,63 @@ class NativeProseViewerExpoView(
|
|
|
142
173
|
emitContentHeightIfNeeded()
|
|
143
174
|
}
|
|
144
175
|
|
|
145
|
-
private fun
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
176
|
+
private fun applyRenderJson() {
|
|
177
|
+
updateCollapsedEmptyState()
|
|
178
|
+
proseView.applyRenderJSON(lastRenderJson ?: "[]")
|
|
179
|
+
proseView.visibility = if (isCollapsedEmptyContent) View.GONE else View.VISIBLE
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private fun updateCollapsedEmptyState() {
|
|
183
|
+
isCollapsedEmptyContent = collapsesWhenEmpty &&
|
|
184
|
+
renderJsonContainsOnlyEmptyParagraphs(lastRenderJson ?: "[]")
|
|
185
|
+
proseView.visibility = if (isCollapsedEmptyContent) View.GONE else View.VISIBLE
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private fun emitContentHeightIfNeeded(
|
|
189
|
+
force: Boolean = false,
|
|
190
|
+
measuredContentHeight: Int? = null
|
|
191
|
+
) {
|
|
192
|
+
val contentHeight = if (isCollapsedEmptyContent) {
|
|
193
|
+
0
|
|
194
|
+
} else {
|
|
195
|
+
(
|
|
196
|
+
measuredContentHeight ?: (measureContentHeightPx() + paddingTop + paddingBottom)
|
|
197
|
+
).coerceAtLeast(0)
|
|
198
|
+
}
|
|
199
|
+
if (contentHeight <= 0 && !isCollapsedEmptyContent) {
|
|
149
200
|
return
|
|
150
201
|
}
|
|
151
202
|
if (!force && contentHeight == lastEmittedContentHeight) {
|
|
152
203
|
return
|
|
153
204
|
}
|
|
154
205
|
lastEmittedContentHeight = contentHeight
|
|
206
|
+
if (suppressContentHeightEventsForTesting) {
|
|
207
|
+
return
|
|
208
|
+
}
|
|
155
209
|
onContentHeightChange(mapOf("contentHeight" to contentHeight))
|
|
156
210
|
}
|
|
157
211
|
|
|
158
212
|
private fun measureContentHeightPx(): Int {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return currentMeasuredHeight
|
|
213
|
+
if (isCollapsedEmptyContent) {
|
|
214
|
+
return 0
|
|
162
215
|
}
|
|
163
216
|
|
|
164
217
|
val availableWidthPx = resolveAvailableWidthPx()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
218
|
+
if (
|
|
219
|
+
proseView.measuredWidth <= 0 ||
|
|
220
|
+
abs(proseView.measuredWidth - availableWidthPx) > 1
|
|
221
|
+
) {
|
|
222
|
+
val childWidthSpec = View.MeasureSpec.makeMeasureSpec(
|
|
223
|
+
availableWidthPx,
|
|
224
|
+
View.MeasureSpec.EXACTLY
|
|
225
|
+
)
|
|
226
|
+
val childHeightSpec = View.MeasureSpec.makeMeasureSpec(
|
|
227
|
+
0,
|
|
228
|
+
View.MeasureSpec.UNSPECIFIED
|
|
229
|
+
)
|
|
230
|
+
proseView.measure(childWidthSpec, childHeightSpec)
|
|
231
|
+
}
|
|
232
|
+
return proseView.resolveAutoGrowHeight()
|
|
169
233
|
}
|
|
170
234
|
|
|
171
235
|
private fun resolveAvailableWidthPx(): Int {
|
|
@@ -191,4 +255,61 @@ class NativeProseViewerExpoView(
|
|
|
191
255
|
true
|
|
192
256
|
}.getOrDefault(false)
|
|
193
257
|
}
|
|
258
|
+
|
|
259
|
+
companion object {
|
|
260
|
+
private const val EMPTY_TEXT_BLOCK_PLACEHOLDER = '\u200B'
|
|
261
|
+
|
|
262
|
+
internal fun renderJsonContainsOnlyEmptyParagraphs(renderJson: String): Boolean {
|
|
263
|
+
val elements = try {
|
|
264
|
+
JSONArray(renderJson)
|
|
265
|
+
} catch (_: Exception) {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (elements.length() == 0) {
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
var hasParagraph = false
|
|
274
|
+
var paragraphIsOpen = false
|
|
275
|
+
|
|
276
|
+
for (index in 0 until elements.length()) {
|
|
277
|
+
val element = elements.optJSONObject(index) ?: return false
|
|
278
|
+
when (element.optString("type", "")) {
|
|
279
|
+
"blockStart" -> {
|
|
280
|
+
if (
|
|
281
|
+
paragraphIsOpen ||
|
|
282
|
+
element.optString("nodeType", "") != "paragraph" ||
|
|
283
|
+
element.optInt("depth", 0) != 0
|
|
284
|
+
) {
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
paragraphIsOpen = true
|
|
288
|
+
hasParagraph = true
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
"textRun" -> {
|
|
292
|
+
val text = element.optString("text", "")
|
|
293
|
+
if (
|
|
294
|
+
!paragraphIsOpen ||
|
|
295
|
+
!text.all { it == EMPTY_TEXT_BLOCK_PLACEHOLDER }
|
|
296
|
+
) {
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
"blockEnd" -> {
|
|
302
|
+
if (!paragraphIsOpen) {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
paragraphIsOpen = false
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
else -> return false
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return hasParagraph && !paragraphIsOpen
|
|
313
|
+
}
|
|
314
|
+
}
|
|
194
315
|
}
|
|
@@ -1465,6 +1465,9 @@ object RenderBridge {
|
|
|
1465
1465
|
val indent = calculateIndent(currentBlock, blockStack, theme, density)
|
|
1466
1466
|
val markerWidth = calculateMarkerWidth(density)
|
|
1467
1467
|
val quoteDepth = blockquoteDepth(blockStack)
|
|
1468
|
+
val indentPerDepth = (theme?.list?.indent ?: LayoutConstants.INDENT_PER_DEPTH) * density
|
|
1469
|
+
val listBaseIndentAdjustment =
|
|
1470
|
+
calculateListBaseIndentAdjustment(currentBlock, theme, density)
|
|
1468
1471
|
val quoteStripeColor = if (quoteDepth > 0) {
|
|
1469
1472
|
theme?.blockquote?.borderColor ?: Color.argb(
|
|
1470
1473
|
(Color.alpha(resolveInlineTextColor(blockStack, Color.BLACK, theme)) * 0.3f).toInt(),
|
|
@@ -1486,8 +1489,9 @@ object RenderBridge {
|
|
|
1486
1489
|
) * density
|
|
1487
1490
|
val blockquoteIndentPx = (quoteDepth * quoteIndent).toInt()
|
|
1488
1491
|
val quoteBaseIndent = if (quoteDepth > 0) {
|
|
1489
|
-
((currentBlock.depth *
|
|
1490
|
-
- (quoteDepth *
|
|
1492
|
+
((currentBlock.depth * indentPerDepth)
|
|
1493
|
+
- (quoteDepth * indentPerDepth)
|
|
1494
|
+
+ listBaseIndentAdjustment
|
|
1491
1495
|
+ ((quoteDepth - 1f) * quoteIndent)).toInt()
|
|
1492
1496
|
} else {
|
|
1493
1497
|
0
|
|
@@ -1666,7 +1670,25 @@ object RenderBridge {
|
|
|
1666
1670
|
(theme?.blockquote?.markerGap ?: LayoutConstants.BLOCKQUOTE_MARKER_GAP) +
|
|
1667
1671
|
(theme?.blockquote?.borderWidth ?: LayoutConstants.BLOCKQUOTE_BORDER_WIDTH)
|
|
1668
1672
|
) * density
|
|
1669
|
-
|
|
1673
|
+
val listBaseIndentAdjustment = calculateListBaseIndentAdjustment(context, theme, density)
|
|
1674
|
+
return (context.depth * indentPerDepth) -
|
|
1675
|
+
(quoteDepth * indentPerDepth) +
|
|
1676
|
+
listBaseIndentAdjustment +
|
|
1677
|
+
(quoteDepth * quoteIndent)
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
private fun calculateListBaseIndentAdjustment(
|
|
1681
|
+
context: BlockContext,
|
|
1682
|
+
theme: EditorTheme?,
|
|
1683
|
+
density: Float
|
|
1684
|
+
): Float {
|
|
1685
|
+
if (context.listContext == null) {
|
|
1686
|
+
return 0f
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
val indentPerDepth = (theme?.list?.indent ?: LayoutConstants.INDENT_PER_DEPTH) * density
|
|
1690
|
+
val listBaseIndentMultiplier = maxOf(theme?.list?.baseIndentMultiplier ?: 1f, 0f)
|
|
1691
|
+
return (listBaseIndentMultiplier - 1f) * indentPerDepth
|
|
1670
1692
|
}
|
|
1671
1693
|
|
|
1672
1694
|
private fun effectiveBlockContext(blockStack: List<BlockContext>): BlockContext? {
|
package/dist/EditorTheme.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface EditorHeadingTheme {
|
|
|
36
36
|
}
|
|
37
37
|
export interface EditorListTheme {
|
|
38
38
|
indent?: number;
|
|
39
|
+
baseIndentMultiplier?: number;
|
|
39
40
|
itemSpacing?: number;
|
|
40
41
|
markerColor?: string;
|
|
41
42
|
markerScale?: number;
|
|
@@ -59,6 +60,8 @@ export interface EditorToolbarTheme {
|
|
|
59
60
|
borderColor?: string;
|
|
60
61
|
borderWidth?: number;
|
|
61
62
|
borderRadius?: number;
|
|
63
|
+
marginTop?: number;
|
|
64
|
+
showTopBorder?: boolean;
|
|
62
65
|
keyboardOffset?: number;
|
|
63
66
|
horizontalInset?: number;
|
|
64
67
|
separatorColor?: string;
|
package/dist/EditorToolbar.js
CHANGED
|
@@ -109,7 +109,7 @@ const DEFAULT_MATERIAL_ICONS = {
|
|
|
109
109
|
undo: 'undo',
|
|
110
110
|
redo: 'redo',
|
|
111
111
|
};
|
|
112
|
-
function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder
|
|
112
|
+
function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder, }) {
|
|
113
113
|
const marks = activeState.marks ?? {};
|
|
114
114
|
const nodes = activeState.nodes ?? {};
|
|
115
115
|
const commands = activeState.commands ?? {};
|
|
@@ -371,6 +371,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
371
371
|
groupsByKey: nextGroups,
|
|
372
372
|
};
|
|
373
373
|
}, [expandedGroupKey, menuState?.groupKey, resolveButton, toolbarItems]);
|
|
374
|
+
const resolvedShowTopBorder = showTopBorder ?? theme?.showTopBorder ?? true;
|
|
374
375
|
(0, react_1.useEffect)(() => {
|
|
375
376
|
if (expandedGroupKey != null && !groupsByKey.has(expandedGroupKey)) {
|
|
376
377
|
setExpandedGroupKey(null);
|
|
@@ -414,7 +415,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
414
415
|
});
|
|
415
416
|
});
|
|
416
417
|
}, []);
|
|
417
|
-
const menuGroup = menuState != null ? groupsByKey.get(menuState.groupKey) ?? null : null;
|
|
418
|
+
const menuGroup = menuState != null ? (groupsByKey.get(menuState.groupKey) ?? null) : null;
|
|
418
419
|
const menuHeight = menuGroup ? menuGroup.children.length * 40 + 16 : 0;
|
|
419
420
|
const menuTop = menuState == null
|
|
420
421
|
? 0
|
|
@@ -426,7 +427,11 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
426
427
|
const activeColor = theme?.buttonActiveColor ?? ACTIVE_COLOR;
|
|
427
428
|
const defaultColor = theme?.buttonColor ?? DEFAULT_COLOR;
|
|
428
429
|
const disabledColor = theme?.buttonDisabledColor ?? DISABLED_COLOR;
|
|
429
|
-
const color = button.isActive
|
|
430
|
+
const color = button.isActive
|
|
431
|
+
? activeColor
|
|
432
|
+
: button.isDisabled
|
|
433
|
+
? disabledColor
|
|
434
|
+
: defaultColor;
|
|
430
435
|
const anchorGroupKey = options?.anchorGroupKey;
|
|
431
436
|
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: anchorGroupKey == null
|
|
432
437
|
? undefined
|
|
@@ -457,15 +462,15 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
457
462
|
] }, key));
|
|
458
463
|
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
|
|
459
464
|
styles.container,
|
|
460
|
-
!
|
|
465
|
+
!resolvedShowTopBorder && styles.containerWithoutTopBorder,
|
|
461
466
|
theme?.backgroundColor != null ? { backgroundColor: theme.backgroundColor } : null,
|
|
462
467
|
theme?.borderColor != null
|
|
463
|
-
?
|
|
468
|
+
? resolvedShowTopBorder
|
|
464
469
|
? { borderTopColor: theme.borderColor }
|
|
465
470
|
: null
|
|
466
471
|
: null,
|
|
467
472
|
theme?.borderWidth != null
|
|
468
|
-
?
|
|
473
|
+
? resolvedShowTopBorder
|
|
469
474
|
? { borderTopWidth: theme.borderWidth }
|
|
470
475
|
: null
|
|
471
476
|
: null,
|
|
@@ -4,6 +4,7 @@ exports.NativeProseViewer = NativeProseViewer;
|
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const expo_modules_core_1 = require("expo-modules-core");
|
|
7
|
+
const react_native_1 = require("react-native");
|
|
7
8
|
const addons_1 = require("./addons");
|
|
8
9
|
const EditorTheme_1 = require("./EditorTheme");
|
|
9
10
|
const schemas_1 = require("./schemas");
|
|
@@ -168,6 +169,59 @@ function isEmptyParagraphPlaceholderText(text) {
|
|
|
168
169
|
}
|
|
169
170
|
return Array.from(text).every((char) => char === EMPTY_TEXT_BLOCK_PLACEHOLDER);
|
|
170
171
|
}
|
|
172
|
+
function isCollapsibleEmptyParagraphText(text) {
|
|
173
|
+
return Array.from(text).every((char) => char === EMPTY_TEXT_BLOCK_PLACEHOLDER);
|
|
174
|
+
}
|
|
175
|
+
function renderElementsJsonContainsOnlyEmptyParagraphs(renderJson) {
|
|
176
|
+
let parsedElements;
|
|
177
|
+
try {
|
|
178
|
+
parsedElements = JSON.parse(renderJson);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(parsedElements)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (parsedElements.length === 0) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
let hasParagraph = false;
|
|
190
|
+
let paragraphIsOpen = false;
|
|
191
|
+
for (const element of parsedElements) {
|
|
192
|
+
if (element == null || typeof element !== 'object' || Array.isArray(element)) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const renderElement = element;
|
|
196
|
+
switch (renderElement.type) {
|
|
197
|
+
case 'blockStart':
|
|
198
|
+
if (paragraphIsOpen ||
|
|
199
|
+
renderElement.nodeType !== 'paragraph' ||
|
|
200
|
+
renderElement.depth !== 0) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
paragraphIsOpen = true;
|
|
204
|
+
hasParagraph = true;
|
|
205
|
+
break;
|
|
206
|
+
case 'textRun':
|
|
207
|
+
if (!paragraphIsOpen ||
|
|
208
|
+
typeof renderElement.text !== 'string' ||
|
|
209
|
+
!isCollapsibleEmptyParagraphText(renderElement.text)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
case 'blockEnd':
|
|
214
|
+
if (!paragraphIsOpen) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
paragraphIsOpen = false;
|
|
218
|
+
break;
|
|
219
|
+
default:
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return hasParagraph && !paragraphIsOpen;
|
|
224
|
+
}
|
|
171
225
|
function isTrailingEmptyParagraphRange(elements, start, endExclusive) {
|
|
172
226
|
const startElement = elements[start];
|
|
173
227
|
const endElement = elements[endExclusive - 1];
|
|
@@ -325,26 +379,22 @@ function NativeProseViewer({ ...props }) {
|
|
|
325
379
|
mentionPayloadsByDocPos,
|
|
326
380
|
serializedContentJson,
|
|
327
381
|
]);
|
|
382
|
+
const renderJsonIsCollapsedEmpty = (0, react_1.useMemo)(() => collapseTrailingEmptyParagraphs &&
|
|
383
|
+
renderElementsJsonContainsOnlyEmptyParagraphs(renderJson), [collapseTrailingEmptyParagraphs, renderJson]);
|
|
328
384
|
const [contentHeight, setContentHeight] = (0, react_1.useState)(null);
|
|
329
|
-
const allowContentHeightShrinkRef = (0, react_1.useRef)(true);
|
|
330
|
-
(0, react_1.useEffect)(() => {
|
|
331
|
-
allowContentHeightShrinkRef.current = true;
|
|
332
|
-
}, [resolvedContentRevision, renderJson, themeJson]);
|
|
333
385
|
const handleContentHeightChange = (0, react_1.useCallback)((event) => {
|
|
334
|
-
const
|
|
335
|
-
|
|
386
|
+
const density = react_native_1.Platform.OS === 'android' ? react_native_1.PixelRatio.get() : 1;
|
|
387
|
+
const nextHeight = Math.ceil(event.nativeEvent.contentHeight / density);
|
|
388
|
+
if (nextHeight < 0)
|
|
389
|
+
return;
|
|
390
|
+
if (nextHeight === 0 && !renderJsonIsCollapsedEmpty)
|
|
391
|
+
return;
|
|
392
|
+
if (nextHeight === 0) {
|
|
393
|
+
setContentHeight((currentHeight) => currentHeight === 0 ? currentHeight : 0);
|
|
336
394
|
return;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
? (() => {
|
|
341
|
-
allowContentHeightShrinkRef.current = false;
|
|
342
|
-
return currentHeight === nextHeight
|
|
343
|
-
? currentHeight
|
|
344
|
-
: nextHeight;
|
|
345
|
-
})()
|
|
346
|
-
: currentHeight);
|
|
347
|
-
}, []);
|
|
395
|
+
}
|
|
396
|
+
setContentHeight((currentHeight) => currentHeight === nextHeight ? currentHeight : nextHeight);
|
|
397
|
+
}, [renderJsonIsCollapsedEmpty]);
|
|
348
398
|
const handlePressMention = (0, react_1.useCallback)((event) => {
|
|
349
399
|
if (!onPressMention)
|
|
350
400
|
return;
|
|
@@ -364,10 +414,19 @@ function NativeProseViewer({ ...props }) {
|
|
|
364
414
|
text: event.nativeEvent.text,
|
|
365
415
|
});
|
|
366
416
|
}, [onPressLink]);
|
|
367
|
-
const nativeStyle = (0, react_1.useMemo)(() =>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
417
|
+
const nativeStyle = (0, react_1.useMemo)(() => {
|
|
418
|
+
let measuredStyle = null;
|
|
419
|
+
if (renderJsonIsCollapsedEmpty) {
|
|
420
|
+
measuredStyle = { height: 0, minHeight: 0 };
|
|
421
|
+
}
|
|
422
|
+
else if (contentHeight != null && contentHeight > 0) {
|
|
423
|
+
measuredStyle = { minHeight: contentHeight };
|
|
424
|
+
}
|
|
425
|
+
return [
|
|
426
|
+
{ minHeight: renderJsonIsCollapsedEmpty ? 0 : 1 },
|
|
427
|
+
style,
|
|
428
|
+
measuredStyle,
|
|
429
|
+
];
|
|
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 onPressMention === 'function' ? handlePressMention : undefined }));
|
|
373
432
|
}
|
|
@@ -16,6 +16,9 @@ const DEV_NATIVE_VIEW_KEY = __DEV__
|
|
|
16
16
|
: 'native-editor';
|
|
17
17
|
const LINK_TOOLBAR_ACTION_KEY = '__native-editor-link__';
|
|
18
18
|
const IMAGE_TOOLBAR_ACTION_KEY = '__native-editor-image__';
|
|
19
|
+
const DEFAULT_MENTION_TRIGGER = '@';
|
|
20
|
+
const MAX_INLINE_MENTION_SUGGESTIONS = 8;
|
|
21
|
+
const INLINE_TOOLBAR_BORDER_COLOR = '#E5E5EA';
|
|
19
22
|
function mapToolbarChildForNative(item, activeState, editable, onRequestLink, onRequestImage) {
|
|
20
23
|
if (item.type === 'link') {
|
|
21
24
|
return {
|
|
@@ -34,7 +37,9 @@ function mapToolbarChildForNative(item, activeState, editable, onRequestLink, on
|
|
|
34
37
|
label: item.label,
|
|
35
38
|
icon: item.icon,
|
|
36
39
|
isActive: false,
|
|
37
|
-
isDisabled: !editable ||
|
|
40
|
+
isDisabled: !editable ||
|
|
41
|
+
!onRequestImage ||
|
|
42
|
+
!activeState.insertableNodes.includes(schemas_1.IMAGE_NODE_NAME),
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
return item;
|
|
@@ -65,6 +70,34 @@ function isPromiseLike(value) {
|
|
|
65
70
|
function isRecord(value) {
|
|
66
71
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
67
72
|
}
|
|
73
|
+
function resolveMentionTrigger(addons) {
|
|
74
|
+
return addons?.mentions?.trigger?.trim() || DEFAULT_MENTION_TRIGGER;
|
|
75
|
+
}
|
|
76
|
+
function resolveMentionSuggestionLabel(suggestion, trigger) {
|
|
77
|
+
return suggestion.label?.trim() || `${trigger}${suggestion.title}`;
|
|
78
|
+
}
|
|
79
|
+
function filterMentionSuggestions(suggestions, query, trigger) {
|
|
80
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
81
|
+
const filtered = normalizedQuery.length === 0
|
|
82
|
+
? suggestions
|
|
83
|
+
: suggestions.filter((suggestion) => {
|
|
84
|
+
const label = resolveMentionSuggestionLabel(suggestion, trigger);
|
|
85
|
+
return (suggestion.title.toLowerCase().includes(normalizedQuery) ||
|
|
86
|
+
label.toLowerCase().includes(normalizedQuery) ||
|
|
87
|
+
suggestion.subtitle?.toLowerCase().includes(normalizedQuery) === true);
|
|
88
|
+
});
|
|
89
|
+
return filtered.slice(0, MAX_INLINE_MENTION_SUGGESTIONS);
|
|
90
|
+
}
|
|
91
|
+
function resolveMentionSuggestionAttrs(suggestion, trigger) {
|
|
92
|
+
const attrs = { ...(suggestion.attrs ?? {}) };
|
|
93
|
+
if (!('label' in attrs)) {
|
|
94
|
+
attrs.label = resolveMentionSuggestionLabel(suggestion, trigger);
|
|
95
|
+
}
|
|
96
|
+
if (!('mentionSuggestionChar' in attrs)) {
|
|
97
|
+
attrs.mentionSuggestionChar = trigger;
|
|
98
|
+
}
|
|
99
|
+
return attrs;
|
|
100
|
+
}
|
|
68
101
|
const AUTO_LINK_URL_REGEX = /(?:https?:\/\/|www\.)\S+/giu;
|
|
69
102
|
const AUTO_LINK_INLINE_PLACEHOLDER = '\uFFFC';
|
|
70
103
|
const AUTO_LINK_LEADING_BOUNDARY_CHARS = new Set(['(', '[', '{', '<', '"', "'"]);
|
|
@@ -142,7 +175,8 @@ function trimAutoLinkTrailingPunctuation(value) {
|
|
|
142
175
|
result = chars.join('');
|
|
143
176
|
continue;
|
|
144
177
|
}
|
|
145
|
-
if ((lastChar === '"' || lastChar === "'") &&
|
|
178
|
+
if ((lastChar === '"' || lastChar === "'") &&
|
|
179
|
+
countOccurrences(result, lastChar) % 2 !== 0) {
|
|
146
180
|
chars.pop();
|
|
147
181
|
result = chars.join('');
|
|
148
182
|
continue;
|
|
@@ -169,7 +203,9 @@ function isAutoLinkBoundaryChar(char) {
|
|
|
169
203
|
if (!char) {
|
|
170
204
|
return true;
|
|
171
205
|
}
|
|
172
|
-
return /\s/u.test(char) ||
|
|
206
|
+
return (/\s/u.test(char) ||
|
|
207
|
+
char === AUTO_LINK_INLINE_PLACEHOLDER ||
|
|
208
|
+
AUTO_LINK_LEADING_BOUNDARY_CHARS.has(char));
|
|
173
209
|
}
|
|
174
210
|
function isAutoLinkTrailingDelimiterChar(char) {
|
|
175
211
|
if (!char) {
|
|
@@ -236,13 +272,15 @@ function findAutoLinkCandidateInInlineBlock(block, cursorDocPos) {
|
|
|
236
272
|
return null;
|
|
237
273
|
}
|
|
238
274
|
let localIndex = 0;
|
|
239
|
-
while (localIndex < block.docPositions.length &&
|
|
275
|
+
while (localIndex < block.docPositions.length &&
|
|
276
|
+
block.docPositions[localIndex] < cursorDocPos) {
|
|
240
277
|
localIndex += 1;
|
|
241
278
|
}
|
|
242
279
|
if (localIndex === 0) {
|
|
243
280
|
return null;
|
|
244
281
|
}
|
|
245
|
-
if (cursorDocPos < block.contentEnd &&
|
|
282
|
+
if (cursorDocPos < block.contentEnd &&
|
|
283
|
+
!isAutoLinkTrailingDelimiterChar(block.chars[localIndex - 1])) {
|
|
246
284
|
return null;
|
|
247
285
|
}
|
|
248
286
|
const prefixChars = block.chars.slice(0, localIndex);
|
|
@@ -444,11 +482,14 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
444
482
|
canUndo: false,
|
|
445
483
|
canRedo: false,
|
|
446
484
|
});
|
|
485
|
+
const [mentionQueryEvent, setMentionQueryEvent] = (0, react_1.useState)(null);
|
|
447
486
|
// Selection and rendered text length refs (non-rendering state)
|
|
448
487
|
const selectionRef = (0, react_1.useRef)({ type: 'text', anchor: 0, head: 0 });
|
|
449
488
|
const renderedTextLengthRef = (0, react_1.useRef)(0);
|
|
450
489
|
const documentVersionRef = (0, react_1.useRef)(null);
|
|
451
490
|
const toolbarRef = (0, react_1.useRef)(null);
|
|
491
|
+
const mentionQueryEventRef = (0, react_1.useRef)(null);
|
|
492
|
+
mentionQueryEventRef.current = mentionQueryEvent;
|
|
452
493
|
const toolbarItemsSerializationCacheRef = (0, react_1.useRef)(null);
|
|
453
494
|
// Stable callback refs to avoid re-renders
|
|
454
495
|
const onContentChangeRef = (0, react_1.useRef)(onContentChange);
|
|
@@ -672,12 +713,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
672
713
|
setIsReady(false);
|
|
673
714
|
};
|
|
674
715
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
675
|
-
}, [
|
|
676
|
-
maxLength,
|
|
677
|
-
syncStateFromUpdate,
|
|
678
|
-
allowBase64Images,
|
|
679
|
-
serializedSchemaJson,
|
|
680
|
-
]);
|
|
716
|
+
}, [maxLength, syncStateFromUpdate, allowBase64Images, serializedSchemaJson]);
|
|
681
717
|
(0, react_1.useEffect)(() => {
|
|
682
718
|
if (value == null)
|
|
683
719
|
return;
|
|
@@ -803,6 +839,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
803
839
|
const handleFocusChange = (0, react_1.useCallback)((event) => {
|
|
804
840
|
const { isFocused: focused } = event.nativeEvent;
|
|
805
841
|
setIsFocused(focused);
|
|
842
|
+
if (!focused) {
|
|
843
|
+
setMentionQueryEvent(null);
|
|
844
|
+
}
|
|
806
845
|
if (focused) {
|
|
807
846
|
onFocusRef.current?.();
|
|
808
847
|
}
|
|
@@ -810,6 +849,12 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
810
849
|
onBlurRef.current?.();
|
|
811
850
|
}
|
|
812
851
|
}, []);
|
|
852
|
+
(0, react_1.useEffect)(() => {
|
|
853
|
+
if (addons?.mentions != null) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
setMentionQueryEvent(null);
|
|
857
|
+
}, [addons?.mentions]);
|
|
813
858
|
const handleContentHeightChange = (0, react_1.useCallback)((event) => {
|
|
814
859
|
if (heightBehavior !== 'autoGrow')
|
|
815
860
|
return;
|
|
@@ -897,6 +942,46 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
897
942
|
}
|
|
898
943
|
onToolbarAction?.(event.nativeEvent.key);
|
|
899
944
|
}, [onToolbarAction, openImageRequest, openLinkRequest]);
|
|
945
|
+
const resolveMentionSelectionAttrs = (0, react_1.useCallback)((selectionEvent) => {
|
|
946
|
+
let resolvedAttrs;
|
|
947
|
+
try {
|
|
948
|
+
resolvedAttrs =
|
|
949
|
+
addonsRef.current?.mentions?.resolveSelectionAttrs?.(selectionEvent);
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
if (__DEV__) {
|
|
953
|
+
console.error('NativeRichTextEditor: mentions.resolveSelectionAttrs threw', error);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return isRecord(resolvedAttrs)
|
|
957
|
+
? { ...selectionEvent.attrs, ...resolvedAttrs }
|
|
958
|
+
: selectionEvent.attrs;
|
|
959
|
+
}, []);
|
|
960
|
+
const handleInlineMentionSuggestionPress = (0, react_1.useCallback)((suggestion) => {
|
|
961
|
+
const mentionQuery = mentionQueryEventRef.current;
|
|
962
|
+
const mentions = addonsRef.current?.mentions;
|
|
963
|
+
if (!mentionQuery ||
|
|
964
|
+
!mentions ||
|
|
965
|
+
!bridgeRef.current ||
|
|
966
|
+
bridgeRef.current.isDestroyed) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const attrs = resolveMentionSelectionAttrs({
|
|
970
|
+
trigger: mentionQuery.trigger,
|
|
971
|
+
suggestion,
|
|
972
|
+
attrs: resolveMentionSuggestionAttrs(suggestion, mentionQuery.trigger),
|
|
973
|
+
range: mentionQuery.range,
|
|
974
|
+
});
|
|
975
|
+
const update = runAndApply(() => bridgeRef.current?.insertContentJsonAtSelectionScalar(mentionQuery.range.anchor, mentionQuery.range.head, (0, addons_1.buildMentionFragmentJson)(attrs)) ?? null);
|
|
976
|
+
if (update) {
|
|
977
|
+
setMentionQueryEvent(null);
|
|
978
|
+
mentions.onSelect?.({
|
|
979
|
+
trigger: mentionQuery.trigger,
|
|
980
|
+
suggestion,
|
|
981
|
+
attrs,
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}, [resolveMentionSelectionAttrs, runAndApply]);
|
|
900
985
|
const handleAddonEvent = (0, react_1.useCallback)((event) => {
|
|
901
986
|
let parsed = null;
|
|
902
987
|
try {
|
|
@@ -908,11 +993,18 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
908
993
|
if (!parsed)
|
|
909
994
|
return;
|
|
910
995
|
if (parsed.type === 'mentionsQueryChange') {
|
|
911
|
-
|
|
996
|
+
const nextEvent = {
|
|
912
997
|
query: parsed.query,
|
|
913
998
|
trigger: parsed.trigger,
|
|
914
999
|
range: parsed.range,
|
|
915
1000
|
isActive: parsed.isActive,
|
|
1001
|
+
};
|
|
1002
|
+
setMentionQueryEvent(parsed.isActive ? nextEvent : null);
|
|
1003
|
+
addonsRef.current?.mentions?.onQueryChange?.({
|
|
1004
|
+
query: nextEvent.query,
|
|
1005
|
+
trigger: nextEvent.trigger,
|
|
1006
|
+
range: nextEvent.range,
|
|
1007
|
+
isActive: nextEvent.isActive,
|
|
916
1008
|
});
|
|
917
1009
|
return;
|
|
918
1010
|
}
|
|
@@ -926,19 +1018,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
926
1018
|
attrs: parsed.attrs,
|
|
927
1019
|
range: parsed.range,
|
|
928
1020
|
};
|
|
929
|
-
|
|
930
|
-
try {
|
|
931
|
-
resolvedAttrs =
|
|
932
|
-
addonsRef.current?.mentions?.resolveSelectionAttrs?.(selectionEvent);
|
|
933
|
-
}
|
|
934
|
-
catch (error) {
|
|
935
|
-
if (__DEV__) {
|
|
936
|
-
console.error('NativeRichTextEditor: mentions.resolveSelectionAttrs threw', error);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
const finalAttrs = isRecord(resolvedAttrs)
|
|
940
|
-
? { ...parsed.attrs, ...resolvedAttrs }
|
|
941
|
-
: parsed.attrs;
|
|
1021
|
+
const finalAttrs = resolveMentionSelectionAttrs(selectionEvent);
|
|
942
1022
|
const update = runAndApply(() => bridgeRef.current?.insertContentJsonAtSelectionScalar(parsed.range.anchor, parsed.range.head, (0, addons_1.buildMentionFragmentJson)(finalAttrs)) ?? null);
|
|
943
1023
|
if (update) {
|
|
944
1024
|
addonsRef.current?.mentions?.onSelect?.({
|
|
@@ -959,7 +1039,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
959
1039
|
attrs: parsed.attrs,
|
|
960
1040
|
});
|
|
961
1041
|
}
|
|
962
|
-
}, [runAndApply]);
|
|
1042
|
+
}, [resolveMentionSelectionAttrs, runAndApply]);
|
|
963
1043
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
964
1044
|
focus() {
|
|
965
1045
|
nativeViewRef.current?.focus?.();
|
|
@@ -1092,6 +1172,25 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1092
1172
|
borderWidth: theme?.toolbar?.borderWidth,
|
|
1093
1173
|
borderRadius: theme?.toolbar?.borderRadius,
|
|
1094
1174
|
};
|
|
1175
|
+
const inlineToolbarMarginTop = theme?.toolbar?.marginTop ?? 8;
|
|
1176
|
+
const inlineToolbarShowTopBorder = theme?.toolbar?.showTopBorder ?? false;
|
|
1177
|
+
const inlineToolbarMentionTheme = theme?.mentions ?? addons?.mentions?.theme;
|
|
1178
|
+
const inlineToolbarContentTopBorderStyle = inlineToolbarShowTopBorder
|
|
1179
|
+
? {
|
|
1180
|
+
borderTopWidth: theme?.toolbar?.borderWidth ?? react_native_1.StyleSheet.hairlineWidth,
|
|
1181
|
+
borderTopColor: theme?.toolbar?.borderColor ?? INLINE_TOOLBAR_BORDER_COLOR,
|
|
1182
|
+
}
|
|
1183
|
+
: null;
|
|
1184
|
+
const inlineMentionSuggestions = toolbarPlacement === 'inline' &&
|
|
1185
|
+
isFocused &&
|
|
1186
|
+
mentionQueryEvent != null &&
|
|
1187
|
+
addons?.mentions != null
|
|
1188
|
+
? filterMentionSuggestions(addons.mentions.suggestions ?? [], mentionQueryEvent.query, mentionQueryEvent.trigger || resolveMentionTrigger(addons))
|
|
1189
|
+
: [];
|
|
1190
|
+
const shouldShowInlineMentionSuggestions = shouldRenderJsToolbar &&
|
|
1191
|
+
toolbarPlacement === 'inline' &&
|
|
1192
|
+
isFocused &&
|
|
1193
|
+
inlineMentionSuggestions.length > 0;
|
|
1095
1194
|
const containerMinHeight = react_native_1.StyleSheet.flatten(containerStyle)?.minHeight;
|
|
1096
1195
|
const nativeViewStyleParts = [];
|
|
1097
1196
|
if (containerMinHeight != null) {
|
|
@@ -1106,6 +1205,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1106
1205
|
const nativeViewStyle = nativeViewStyleParts.length <= 1 ? nativeViewStyleParts[0] : nativeViewStyleParts;
|
|
1107
1206
|
const jsToolbar = ((0, jsx_runtime_1.jsx)(react_native_1.View, { ref: toolbarRef, testID: 'native-editor-js-toolbar', style: [
|
|
1108
1207
|
styles.inlineToolbar,
|
|
1208
|
+
{ marginTop: inlineToolbarMarginTop },
|
|
1109
1209
|
inlineToolbarChrome.backgroundColor != null
|
|
1110
1210
|
? { backgroundColor: inlineToolbarChrome.backgroundColor }
|
|
1111
1211
|
: null,
|
|
@@ -1118,7 +1218,43 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1118
1218
|
inlineToolbarChrome.borderRadius != null
|
|
1119
1219
|
? { borderRadius: inlineToolbarChrome.borderRadius }
|
|
1120
1220
|
: null,
|
|
1121
|
-
], onLayout: updateToolbarFrame, children: (0, jsx_runtime_1.jsx)(
|
|
1221
|
+
], onLayout: updateToolbarFrame, children: shouldShowInlineMentionSuggestions ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: 'native-editor-inline-mention-suggestions', style: [
|
|
1222
|
+
styles.inlineMentionSuggestionsContainer,
|
|
1223
|
+
inlineToolbarContentTopBorderStyle,
|
|
1224
|
+
], children: (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.inlineMentionSuggestionsContent, keyboardShouldPersistTaps: 'always', children: inlineMentionSuggestions.map((suggestion) => {
|
|
1225
|
+
const label = resolveMentionSuggestionLabel(suggestion, mentionQueryEvent?.trigger ?? resolveMentionTrigger(addons));
|
|
1226
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `native-editor-inline-mention-suggestion-${suggestion.key}`, onPress: () => handleInlineMentionSuggestionPress(suggestion), accessibilityRole: 'button', accessibilityLabel: label, style: ({ pressed }) => [
|
|
1227
|
+
styles.inlineMentionSuggestion,
|
|
1228
|
+
{
|
|
1229
|
+
backgroundColor: pressed
|
|
1230
|
+
? (inlineToolbarMentionTheme?.optionHighlightedBackgroundColor ??
|
|
1231
|
+
'rgba(0, 122, 255, 0.12)')
|
|
1232
|
+
: (inlineToolbarMentionTheme?.backgroundColor ??
|
|
1233
|
+
'#F2F2F7'),
|
|
1234
|
+
borderColor: inlineToolbarMentionTheme?.borderColor ??
|
|
1235
|
+
'transparent',
|
|
1236
|
+
borderWidth: inlineToolbarMentionTheme?.borderWidth ?? 0,
|
|
1237
|
+
borderRadius: inlineToolbarMentionTheme?.borderRadius ?? 12,
|
|
1238
|
+
},
|
|
1239
|
+
], children: ({ pressed }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
|
|
1240
|
+
styles.inlineMentionSuggestionTitle,
|
|
1241
|
+
{
|
|
1242
|
+
color: pressed
|
|
1243
|
+
? (inlineToolbarMentionTheme?.optionHighlightedTextColor ??
|
|
1244
|
+
inlineToolbarMentionTheme?.optionTextColor ??
|
|
1245
|
+
'#000000')
|
|
1246
|
+
: (inlineToolbarMentionTheme?.optionTextColor ??
|
|
1247
|
+
inlineToolbarMentionTheme?.textColor ??
|
|
1248
|
+
'#000000'),
|
|
1249
|
+
},
|
|
1250
|
+
], children: label }), suggestion.subtitle ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
|
|
1251
|
+
styles.inlineMentionSuggestionSubtitle,
|
|
1252
|
+
{
|
|
1253
|
+
color: inlineToolbarMentionTheme?.optionSecondaryTextColor ??
|
|
1254
|
+
'#8E8E93',
|
|
1255
|
+
},
|
|
1256
|
+
], children: suggestion.subtitle })) : null] })) }, suggestion.key));
|
|
1257
|
+
}) }) })) : ((0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: inlineToolbarShowTopBorder, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
|
|
1122
1258
|
skipNativeApplyIfContentUnchanged: true,
|
|
1123
1259
|
}), onToggleListType: (listType) => runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null), onToggleHeading: (level) => runAndApply(() => bridgeRef.current?.toggleHeading(level) ?? null), onToggleBlockquote: () => runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null), onInsertNodeType: (nodeType) => runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null), onRunCommand: (command) => {
|
|
1124
1260
|
switch (command) {
|
|
@@ -1143,7 +1279,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1143
1279
|
skipNativeApplyIfContentUnchanged: true,
|
|
1144
1280
|
}), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
|
|
1145
1281
|
skipNativeApplyIfContentUnchanged: true,
|
|
1146
|
-
}), onToggleBulletList: () => runAndApply(() => bridgeRef.current?.toggleList('bulletList') ?? null), onToggleOrderedList: () => runAndApply(() => bridgeRef.current?.toggleList('orderedList') ?? null), onIndentList: () => runAndApply(() => bridgeRef.current?.indentListItem() ?? null), onOutdentList: () => runAndApply(() => bridgeRef.current?.outdentListItem() ?? null), onInsertHorizontalRule: () => runAndApply(() => bridgeRef.current?.insertNode('horizontalRule') ?? null), onInsertLineBreak: () => runAndApply(() => bridgeRef.current?.insertNode('hardBreak') ?? null), onUndo: () => runAndApply(() => bridgeRef.current?.undo() ?? null), onRedo: () => runAndApply(() => bridgeRef.current?.redo() ?? null) }) }));
|
|
1282
|
+
}), onToggleBulletList: () => runAndApply(() => bridgeRef.current?.toggleList('bulletList') ?? null), onToggleOrderedList: () => runAndApply(() => bridgeRef.current?.toggleList('orderedList') ?? null), onIndentList: () => runAndApply(() => bridgeRef.current?.indentListItem() ?? null), onOutdentList: () => runAndApply(() => bridgeRef.current?.outdentListItem() ?? null), onInsertHorizontalRule: () => runAndApply(() => bridgeRef.current?.insertNode('horizontalRule') ?? null), onInsertLineBreak: () => runAndApply(() => bridgeRef.current?.insertNode('hardBreak') ?? null), onUndo: () => runAndApply(() => bridgeRef.current?.undo() ?? null), onRedo: () => runAndApply(() => bridgeRef.current?.redo() ?? null) })) }));
|
|
1147
1283
|
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarPlacement === 'inline' && isFocused ? toolbarFrameJson : undefined, editorUpdateJson: pendingNativeUpdate.json, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
|
|
1148
1284
|
});
|
|
1149
1285
|
const styles = react_native_1.StyleSheet.create({
|
|
@@ -1151,9 +1287,32 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
1151
1287
|
position: 'relative',
|
|
1152
1288
|
},
|
|
1153
1289
|
inlineToolbar: {
|
|
1154
|
-
marginTop: 8,
|
|
1155
1290
|
borderWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
1156
|
-
borderColor:
|
|
1291
|
+
borderColor: INLINE_TOOLBAR_BORDER_COLOR,
|
|
1292
|
+
overflow: 'hidden',
|
|
1293
|
+
},
|
|
1294
|
+
inlineMentionSuggestionsContainer: {
|
|
1157
1295
|
overflow: 'hidden',
|
|
1158
1296
|
},
|
|
1297
|
+
inlineMentionSuggestionsContent: {
|
|
1298
|
+
paddingHorizontal: 12,
|
|
1299
|
+
paddingVertical: 8,
|
|
1300
|
+
alignItems: 'center',
|
|
1301
|
+
},
|
|
1302
|
+
inlineMentionSuggestion: {
|
|
1303
|
+
minWidth: 88,
|
|
1304
|
+
minHeight: 40,
|
|
1305
|
+
marginRight: 8,
|
|
1306
|
+
paddingHorizontal: 12,
|
|
1307
|
+
paddingVertical: 8,
|
|
1308
|
+
justifyContent: 'center',
|
|
1309
|
+
},
|
|
1310
|
+
inlineMentionSuggestionTitle: {
|
|
1311
|
+
fontSize: 14,
|
|
1312
|
+
fontWeight: '600',
|
|
1313
|
+
},
|
|
1314
|
+
inlineMentionSuggestionSubtitle: {
|
|
1315
|
+
marginTop: 1,
|
|
1316
|
+
fontSize: 12,
|
|
1317
|
+
},
|
|
1159
1318
|
});
|
|
Binary file
|
|
Binary file
|
package/ios/EditorTheme.swift
CHANGED
|
@@ -80,12 +80,14 @@ struct EditorTextStyle {
|
|
|
80
80
|
|
|
81
81
|
struct EditorListTheme {
|
|
82
82
|
var indent: CGFloat?
|
|
83
|
+
var baseIndentMultiplier: CGFloat?
|
|
83
84
|
var itemSpacing: CGFloat?
|
|
84
85
|
var markerColor: UIColor?
|
|
85
86
|
var markerScale: CGFloat?
|
|
86
87
|
|
|
87
88
|
init(dictionary: [String: Any]) {
|
|
88
89
|
indent = EditorTheme.cgFloat(dictionary["indent"])
|
|
90
|
+
baseIndentMultiplier = EditorTheme.cgFloat(dictionary["baseIndentMultiplier"])
|
|
89
91
|
itemSpacing = EditorTheme.cgFloat(dictionary["itemSpacing"])
|
|
90
92
|
markerColor = EditorTheme.color(from: dictionary["markerColor"])
|
|
91
93
|
markerScale = EditorTheme.cgFloat(dictionary["markerScale"])
|
|
@@ -194,6 +196,8 @@ struct EditorToolbarTheme {
|
|
|
194
196
|
var borderColor: UIColor?
|
|
195
197
|
var borderWidth: CGFloat?
|
|
196
198
|
var borderRadius: CGFloat?
|
|
199
|
+
var marginTop: CGFloat?
|
|
200
|
+
var showTopBorder: Bool?
|
|
197
201
|
var keyboardOffset: CGFloat?
|
|
198
202
|
var horizontalInset: CGFloat?
|
|
199
203
|
var separatorColor: UIColor?
|
|
@@ -209,6 +213,8 @@ struct EditorToolbarTheme {
|
|
|
209
213
|
borderColor = EditorTheme.color(from: dictionary["borderColor"])
|
|
210
214
|
borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
|
|
211
215
|
borderRadius = EditorTheme.cgFloat(dictionary["borderRadius"])
|
|
216
|
+
marginTop = EditorTheme.cgFloat(dictionary["marginTop"])
|
|
217
|
+
showTopBorder = dictionary["showTopBorder"] as? Bool
|
|
212
218
|
keyboardOffset = EditorTheme.cgFloat(dictionary["keyboardOffset"])
|
|
213
219
|
horizontalInset = EditorTheme.cgFloat(dictionary["horizontalInset"])
|
|
214
220
|
separatorColor = EditorTheme.color(from: dictionary["separatorColor"])
|
|
@@ -390,6 +390,10 @@ public class NativeEditorModule: Module {
|
|
|
390
390
|
Prop("themeJson") { (view: NativeProseViewerExpoView, themeJson: String?) in
|
|
391
391
|
view.setThemeJson(themeJson)
|
|
392
392
|
}
|
|
393
|
+
Prop("collapsesWhenEmpty") {
|
|
394
|
+
(view: NativeProseViewerExpoView, collapsesWhenEmpty: Bool?) in
|
|
395
|
+
view.setCollapsesWhenEmpty(collapsesWhenEmpty)
|
|
396
|
+
}
|
|
393
397
|
Prop("enableLinkTaps") { (view: NativeProseViewerExpoView, enableLinkTaps: Bool?) in
|
|
394
398
|
view.setEnableLinkTaps(enableLinkTaps)
|
|
395
399
|
}
|
|
@@ -11,7 +11,8 @@ final class NativeProseViewerExpoView: ExpoView {
|
|
|
11
11
|
private var lastThemeJSON: String?
|
|
12
12
|
private var lastEmittedContentHeight: CGFloat = 0
|
|
13
13
|
private var lastMeasuredWidth: CGFloat = 0
|
|
14
|
-
private var
|
|
14
|
+
private var collapsesWhenEmpty = true
|
|
15
|
+
private var isCollapsedEmptyContent = false
|
|
15
16
|
private var enableLinkTaps = true
|
|
16
17
|
private var interceptLinkTaps = false
|
|
17
18
|
|
|
@@ -51,17 +52,24 @@ final class NativeProseViewerExpoView: ExpoView {
|
|
|
51
52
|
interceptLinkTaps = intercept ?? false
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
func setCollapsesWhenEmpty(_ collapses: Bool?) {
|
|
56
|
+
let nextValue = collapses ?? true
|
|
57
|
+
guard collapsesWhenEmpty != nextValue else { return }
|
|
58
|
+
collapsesWhenEmpty = nextValue
|
|
59
|
+
updateCollapsedEmptyState()
|
|
60
|
+
setNeedsLayout()
|
|
61
|
+
emitContentHeightIfNeeded(force: true)
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
func setRenderJson(_ renderJson: String?) {
|
|
55
65
|
guard lastRenderJSON != renderJson else { return }
|
|
56
66
|
lastRenderJSON = renderJson
|
|
57
|
-
allowContentHeightShrink = true
|
|
58
67
|
applyRenderJSON()
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
func setThemeJson(_ themeJson: String?) {
|
|
62
71
|
guard lastThemeJSON != themeJson else { return }
|
|
63
72
|
lastThemeJSON = themeJson
|
|
64
|
-
allowContentHeightShrink = true
|
|
65
73
|
let theme = EditorTheme.from(json: themeJson)
|
|
66
74
|
textView.applyTheme(theme)
|
|
67
75
|
let cornerRadius = theme?.borderRadius ?? 0
|
|
@@ -71,6 +79,9 @@ final class NativeProseViewerExpoView: ExpoView {
|
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
override var intrinsicContentSize: CGSize {
|
|
82
|
+
if isCollapsedEmptyContent {
|
|
83
|
+
return CGSize(width: UIView.noIntrinsicMetric, height: 0)
|
|
84
|
+
}
|
|
74
85
|
guard lastEmittedContentHeight > 0 else {
|
|
75
86
|
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
76
87
|
}
|
|
@@ -79,8 +90,13 @@ final class NativeProseViewerExpoView: ExpoView {
|
|
|
79
90
|
|
|
80
91
|
override func layoutSubviews() {
|
|
81
92
|
super.layoutSubviews()
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
if isCollapsedEmptyContent {
|
|
94
|
+
textView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 0)
|
|
95
|
+
textView.updateAutoGrowHostHeight(0)
|
|
96
|
+
} else {
|
|
97
|
+
textView.frame = bounds
|
|
98
|
+
textView.updateAutoGrowHostHeight(bounds.height)
|
|
99
|
+
}
|
|
84
100
|
|
|
85
101
|
let currentWidth = ceil(bounds.width)
|
|
86
102
|
guard abs(currentWidth - lastMeasuredWidth) > 0.5 else { return }
|
|
@@ -89,23 +105,34 @@ final class NativeProseViewerExpoView: ExpoView {
|
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
private func applyRenderJSON() {
|
|
108
|
+
updateCollapsedEmptyState()
|
|
92
109
|
textView.applyRenderJSON(lastRenderJSON ?? "[]")
|
|
93
|
-
|
|
110
|
+
textView.isHidden = isCollapsedEmptyContent
|
|
111
|
+
lastMeasuredWidth = 0
|
|
112
|
+
invalidateIntrinsicContentSize()
|
|
113
|
+
setNeedsLayout()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func updateCollapsedEmptyState() {
|
|
117
|
+
isCollapsedEmptyContent = collapsesWhenEmpty
|
|
118
|
+
&& Self.renderJsonContainsOnlyEmptyParagraphs(lastRenderJSON ?? "[]")
|
|
119
|
+
textView.isHidden = isCollapsedEmptyContent
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
private func emitContentHeightIfNeeded(
|
|
97
123
|
measuredHeight: CGFloat? = nil,
|
|
98
124
|
force: Bool = false
|
|
99
125
|
) {
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
let contentHeight: CGFloat
|
|
127
|
+
if isCollapsedEmptyContent {
|
|
128
|
+
contentHeight = 0
|
|
129
|
+
} else {
|
|
130
|
+
guard bounds.width > 0 else { return }
|
|
131
|
+
let fittedHeight = measuredHeight
|
|
132
|
+
?? textView.measuredAutoGrowHeightForTesting(width: bounds.width)
|
|
133
|
+
contentHeight = ceil(fittedHeight)
|
|
134
|
+
guard contentHeight > 0 else { return }
|
|
135
|
+
}
|
|
109
136
|
guard force || abs(contentHeight - lastEmittedContentHeight) > 0.5 else { return }
|
|
110
137
|
lastEmittedContentHeight = contentHeight
|
|
111
138
|
invalidateIntrinsicContentSize()
|
|
@@ -194,4 +221,56 @@ final class NativeProseViewerExpoView: ExpoView {
|
|
|
194
221
|
guard let url = URL(string: href) else { return }
|
|
195
222
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
196
223
|
}
|
|
224
|
+
|
|
225
|
+
static func renderJsonContainsOnlyEmptyParagraphs(_ renderJson: String) -> Bool {
|
|
226
|
+
guard let data = renderJson.data(using: .utf8),
|
|
227
|
+
let elements = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]]
|
|
228
|
+
else {
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if elements.isEmpty {
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
var hasParagraph = false
|
|
237
|
+
var paragraphIsOpen = false
|
|
238
|
+
|
|
239
|
+
for element in elements {
|
|
240
|
+
guard let type = element["type"] as? String else {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
switch type {
|
|
245
|
+
case "blockStart":
|
|
246
|
+
guard !paragraphIsOpen,
|
|
247
|
+
element["nodeType"] as? String == "paragraph",
|
|
248
|
+
(element["depth"] as? NSNumber)?.intValue == 0
|
|
249
|
+
else {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
paragraphIsOpen = true
|
|
253
|
+
hasParagraph = true
|
|
254
|
+
|
|
255
|
+
case "textRun":
|
|
256
|
+
guard paragraphIsOpen,
|
|
257
|
+
let text = element["text"] as? String,
|
|
258
|
+
text.allSatisfy({ $0 == "\u{200B}" })
|
|
259
|
+
else {
|
|
260
|
+
return false
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case "blockEnd":
|
|
264
|
+
guard paragraphIsOpen else {
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
paragraphIsOpen = false
|
|
268
|
+
|
|
269
|
+
default:
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return hasParagraph && !paragraphIsOpen
|
|
275
|
+
}
|
|
197
276
|
}
|
package/ios/RenderBridge.swift
CHANGED
|
@@ -847,8 +847,13 @@ final class RenderBridge {
|
|
|
847
847
|
(theme?.blockquote?.markerGap ?? LayoutConstants.blockquoteMarkerGap)
|
|
848
848
|
+ (theme?.blockquote?.borderWidth ?? LayoutConstants.blockquoteBorderWidth)
|
|
849
849
|
)
|
|
850
|
+
let listBaseIndentMultiplier = max(theme?.list?.baseIndentMultiplier ?? 1, 0)
|
|
851
|
+
let listBaseIndentAdjustment = context.listContext != nil
|
|
852
|
+
? ((listBaseIndentMultiplier - 1) * indentPerDepth)
|
|
853
|
+
: 0
|
|
850
854
|
let baseIndent = (CGFloat(context.depth) * indentPerDepth)
|
|
851
855
|
- (quoteDepth * indentPerDepth)
|
|
856
|
+
+ listBaseIndentAdjustment
|
|
852
857
|
+ (quoteDepth * quoteIndent)
|
|
853
858
|
|
|
854
859
|
if context.listContext != nil {
|
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.5",
|
|
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
|