@apollohg/react-native-prose-editor 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -15
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +4 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +33 -1
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +39 -6
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +15 -1
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +44 -7
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +24 -4
- package/dist/NativeEditorBridge.d.ts +8 -0
- package/dist/NativeEditorBridge.js +16 -0
- package/dist/NativeProseViewer.d.ts +25 -5
- package/dist/NativeProseViewer.js +212 -13
- package/dist/NativeRichTextEditor.d.ts +2 -0
- package/dist/NativeRichTextEditor.js +417 -31
- package/dist/addons.d.ts +20 -0
- package/dist/addons.js +4 -0
- package/dist/index.d.ts +2 -2
- package/ios/EditorAddons.swift +2 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +10 -1
- package/ios/EditorTheme.swift +25 -0
- package/ios/NativeEditorExpoView.swift +56 -6
- package/ios/NativeEditorModule.swift +14 -1
- package/ios/NativeProseViewerExpoView.swift +62 -11
- package/ios/RenderBridge.swift +40 -16
- package/ios/RichTextEditorView.swift +4 -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
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
This project is currently in `alpha` and the API, behavior, and packaging may still change.
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<img src="
|
|
9
|
-
<img src="
|
|
8
|
+
<img src="https://raw.githubusercontent.com/wiki/apollohg/react-native-prose-editor/images/example-ios.png" alt="Example editor iOS" width="45%" align="top" />
|
|
9
|
+
<img src="https://raw.githubusercontent.com/wiki/apollohg/react-native-prose-editor/images/example-android.png" alt="Example editor Android" width="45%" align="top" />
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
This repository contains three main pieces:
|
|
@@ -38,7 +38,8 @@ The editor already supports:
|
|
|
38
38
|
- [`android`](./android): Android native view, rendering bridge, and Expo module wiring
|
|
39
39
|
- [`Rust Editor Core`](./rust/editor-core): document model, transforms, schema system, selection, history, serialization, and tests
|
|
40
40
|
- [`example`](./example): Expo 54 app for manual QA and development
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
Project documentation now lives in the [GitHub Wiki](https://github.com/apollohg/react-native-prose-editor/wiki).
|
|
42
43
|
|
|
43
44
|
## Installation
|
|
44
45
|
|
|
@@ -67,7 +68,7 @@ npm --prefix example install
|
|
|
67
68
|
npm run example:prebuild
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
For full setup details, including peer dependencies, example app setup, and iOS pods, see the [Installation Guide](
|
|
71
|
+
For full setup details, including peer dependencies, example app setup, and iOS pods, see the [Installation Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Installation).
|
|
71
72
|
|
|
72
73
|
## Basic Usage
|
|
73
74
|
|
|
@@ -106,9 +107,9 @@ The main extension points today are:
|
|
|
106
107
|
- `addons`: configure optional features like @-mentions
|
|
107
108
|
- `heightBehavior`: switch between internal scrolling and auto-grow
|
|
108
109
|
|
|
109
|
-
For setup and customization details, start with the [Documentation Index](
|
|
110
|
+
For setup and customization details, start with the [Documentation Index](https://github.com/apollohg/react-native-prose-editor/wiki).
|
|
110
111
|
|
|
111
|
-
For realtime collaboration, including the correct `useYjsCollaboration()` wiring, encoded-state persistence, remote cursors, and automatic reconnect behavior, see the [Collaboration Guide](
|
|
112
|
+
For realtime collaboration, including the correct `useYjsCollaboration()` wiring, encoded-state persistence, remote cursors, and automatic reconnect behavior, see the [Collaboration Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Collaboration).
|
|
112
113
|
|
|
113
114
|
For whole-document JSON loads, `initialJSON`, controlled `valueJSON`, and `setContentJson()` will normalize an empty root document like `{ type: 'doc', content: [] }` to the active schema's empty text block so block-constrained schemas still load a valid empty document.
|
|
114
115
|
|
|
@@ -152,15 +153,17 @@ npm run ios:test:perf:device
|
|
|
152
153
|
|
|
153
154
|
## Documentation
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- [
|
|
158
|
-
- [
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
161
|
-
- [
|
|
162
|
-
- [
|
|
163
|
-
- [
|
|
156
|
+
Documentation is published in the [GitHub Wiki](https://github.com/apollohg/react-native-prose-editor/wiki).
|
|
157
|
+
|
|
158
|
+
- [Documentation Index](https://github.com/apollohg/react-native-prose-editor/wiki): main documentation index
|
|
159
|
+
- [Installation Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Installation): installation and local setup
|
|
160
|
+
- [Getting Started](https://github.com/apollohg/react-native-prose-editor/wiki/Getting-Started): first setup and first editor
|
|
161
|
+
- [Collaboration Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Collaboration): Yjs collaboration wiring, source-of-truth rules, and persistence
|
|
162
|
+
- [Toolbar Setup](https://github.com/apollohg/react-native-prose-editor/wiki/Toolbar-Setup): toolbar setup patterns and examples
|
|
163
|
+
- [Mentions Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Mentions): @-mentions addon setup and configuration
|
|
164
|
+
- [Styling Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Styling): content, toolbar, and mention styling
|
|
165
|
+
- [NativeRichTextEditor Reference](https://github.com/apollohg/react-native-prose-editor/wiki/NativeRichTextEditor-Reference): component props and ref methods
|
|
166
|
+
- [Design Decisions](https://github.com/apollohg/react-native-prose-editor/wiki/Design-Decisions): rationale for key API and architecture decisions
|
|
164
167
|
|
|
165
168
|
## Project Status
|
|
166
169
|
|
|
@@ -30,7 +30,8 @@ data class NativeMentionSuggestion(
|
|
|
30
30
|
data class NativeMentionsAddonConfig(
|
|
31
31
|
val trigger: String,
|
|
32
32
|
val suggestions: List<NativeMentionSuggestion>,
|
|
33
|
-
val theme: EditorMentionTheme
|
|
33
|
+
val theme: EditorMentionTheme?,
|
|
34
|
+
val resolveSelectionAttrs: Boolean
|
|
34
35
|
) {
|
|
35
36
|
companion object {
|
|
36
37
|
fun fromJson(json: JSONObject?): NativeMentionsAddonConfig? {
|
|
@@ -51,7 +52,8 @@ data class NativeMentionsAddonConfig(
|
|
|
51
52
|
return NativeMentionsAddonConfig(
|
|
52
53
|
trigger = trigger,
|
|
53
54
|
suggestions = suggestions,
|
|
54
|
-
theme = EditorMentionTheme.fromJson(json.optJSONObject("theme"))
|
|
55
|
+
theme = EditorMentionTheme.fromJson(json.optJSONObject("theme")),
|
|
56
|
+
resolveSelectionAttrs = json.optBoolean("resolveSelectionAttrs", false)
|
|
55
57
|
)
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -77,6 +77,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
77
77
|
val label: String
|
|
78
78
|
)
|
|
79
79
|
|
|
80
|
+
data class LinkHit(
|
|
81
|
+
val href: String,
|
|
82
|
+
val text: String
|
|
83
|
+
)
|
|
84
|
+
|
|
80
85
|
private data class ParsedRenderPatch(
|
|
81
86
|
val startIndex: Int,
|
|
82
87
|
val deleteCount: Int,
|
|
@@ -1583,7 +1588,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1583
1588
|
}
|
|
1584
1589
|
}
|
|
1585
1590
|
|
|
1586
|
-
fun
|
|
1591
|
+
private fun textOffsetHitAt(x: Float, y: Float): Pair<Spanned, Int>? {
|
|
1587
1592
|
val spannable = text as? Spanned ?: return null
|
|
1588
1593
|
val layout = layout ?: return null
|
|
1589
1594
|
if (spannable.isEmpty()) return null
|
|
@@ -1603,6 +1608,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1603
1608
|
|
|
1604
1609
|
val offset = layout.getOffsetForHorizontal(line, localX)
|
|
1605
1610
|
.coerceIn(0, maxOf(spannable.length - 1, 0))
|
|
1611
|
+
return spannable to offset
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
fun mentionHitAt(x: Float, y: Float): MentionHit? {
|
|
1615
|
+
val (spannable, offset) = textOffsetHitAt(x, y) ?: return null
|
|
1606
1616
|
val annotations = spannable.getSpans(
|
|
1607
1617
|
offset,
|
|
1608
1618
|
(offset + 1).coerceAtMost(spannable.length),
|
|
@@ -1626,6 +1636,28 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1626
1636
|
)
|
|
1627
1637
|
}
|
|
1628
1638
|
|
|
1639
|
+
fun linkHitAt(x: Float, y: Float): LinkHit? {
|
|
1640
|
+
val (spannable, offset) = textOffsetHitAt(x, y) ?: return null
|
|
1641
|
+
val annotations = spannable.getSpans(
|
|
1642
|
+
offset,
|
|
1643
|
+
(offset + 1).coerceAtMost(spannable.length),
|
|
1644
|
+
Annotation::class.java
|
|
1645
|
+
)
|
|
1646
|
+
val linkAnnotation = annotations.firstOrNull {
|
|
1647
|
+
it.key == RenderBridge.NATIVE_LINK_HREF_ANNOTATION && it.value.isNotBlank()
|
|
1648
|
+
} ?: return null
|
|
1649
|
+
val start = spannable.getSpanStart(linkAnnotation)
|
|
1650
|
+
val end = spannable.getSpanEnd(linkAnnotation)
|
|
1651
|
+
if (start < 0 || end <= start) {
|
|
1652
|
+
return null
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
return LinkHit(
|
|
1656
|
+
href = linkAnnotation.value,
|
|
1657
|
+
text = spannable.subSequence(start, end).toString()
|
|
1658
|
+
)
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1629
1661
|
private fun handleImageTap(event: MotionEvent): Boolean {
|
|
1630
1662
|
if (!imageResizingEnabled) {
|
|
1631
1663
|
return false
|
|
@@ -127,6 +127,29 @@ data class EditorMentionTheme(
|
|
|
127
127
|
val optionHighlightedBackgroundColor: Int? = null,
|
|
128
128
|
val optionHighlightedTextColor: Int? = null
|
|
129
129
|
) {
|
|
130
|
+
fun mergedWith(other: EditorMentionTheme?): EditorMentionTheme {
|
|
131
|
+
other ?: return this
|
|
132
|
+
return copy(
|
|
133
|
+
textColor = other.textColor ?: textColor,
|
|
134
|
+
backgroundColor = other.backgroundColor ?: backgroundColor,
|
|
135
|
+
borderColor = other.borderColor ?: borderColor,
|
|
136
|
+
borderWidth = other.borderWidth ?: borderWidth,
|
|
137
|
+
borderRadius = other.borderRadius ?: borderRadius,
|
|
138
|
+
fontWeight = other.fontWeight ?: fontWeight,
|
|
139
|
+
popoverBackgroundColor = other.popoverBackgroundColor ?: popoverBackgroundColor,
|
|
140
|
+
popoverBorderColor = other.popoverBorderColor ?: popoverBorderColor,
|
|
141
|
+
popoverBorderWidth = other.popoverBorderWidth ?: popoverBorderWidth,
|
|
142
|
+
popoverBorderRadius = other.popoverBorderRadius ?: popoverBorderRadius,
|
|
143
|
+
popoverShadowColor = other.popoverShadowColor ?: popoverShadowColor,
|
|
144
|
+
optionTextColor = other.optionTextColor ?: optionTextColor,
|
|
145
|
+
optionSecondaryTextColor = other.optionSecondaryTextColor ?: optionSecondaryTextColor,
|
|
146
|
+
optionHighlightedBackgroundColor =
|
|
147
|
+
other.optionHighlightedBackgroundColor ?: optionHighlightedBackgroundColor,
|
|
148
|
+
optionHighlightedTextColor =
|
|
149
|
+
other.optionHighlightedTextColor ?: optionHighlightedTextColor
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
130
153
|
companion object {
|
|
131
154
|
fun fromJson(json: JSONObject?): EditorMentionTheme? {
|
|
132
155
|
json ?: return null
|
|
@@ -529,12 +529,42 @@ class NativeEditorExpoView(
|
|
|
529
529
|
onAddonEvent(mapOf("eventJson" to eventJson))
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
private fun
|
|
532
|
+
private fun resolvedMentionAttrs(
|
|
533
|
+
trigger: String,
|
|
534
|
+
suggestion: NativeMentionSuggestion
|
|
535
|
+
): JSONObject {
|
|
536
|
+
val attrs = JSONObject(suggestion.attrs.toString())
|
|
537
|
+
if (!attrs.has("label")) {
|
|
538
|
+
attrs.put("label", suggestion.label)
|
|
539
|
+
}
|
|
540
|
+
if (!attrs.has("mentionSuggestionChar")) {
|
|
541
|
+
attrs.put("mentionSuggestionChar", trigger)
|
|
542
|
+
}
|
|
543
|
+
return attrs
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private fun emitMentionSelect(trigger: String, suggestion: NativeMentionSuggestion, attrs: JSONObject) {
|
|
533
547
|
val eventJson = JSONObject()
|
|
534
548
|
.put("type", "mentionsSelect")
|
|
535
549
|
.put("trigger", trigger)
|
|
536
550
|
.put("suggestionKey", suggestion.key)
|
|
537
|
-
.put("attrs",
|
|
551
|
+
.put("attrs", attrs)
|
|
552
|
+
.toString()
|
|
553
|
+
onAddonEvent(mapOf("eventJson" to eventJson))
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private fun emitMentionSelectRequest(
|
|
557
|
+
trigger: String,
|
|
558
|
+
suggestion: NativeMentionSuggestion,
|
|
559
|
+
attrs: JSONObject,
|
|
560
|
+
range: MentionQueryState
|
|
561
|
+
) {
|
|
562
|
+
val eventJson = JSONObject()
|
|
563
|
+
.put("type", "mentionsSelectRequest")
|
|
564
|
+
.put("trigger", trigger)
|
|
565
|
+
.put("suggestionKey", suggestion.key)
|
|
566
|
+
.put("attrs", attrs)
|
|
567
|
+
.put("range", JSONObject().put("anchor", range.anchor).put("head", range.head))
|
|
538
568
|
.toString()
|
|
539
569
|
onAddonEvent(mapOf("eventJson" to eventJson))
|
|
540
570
|
}
|
|
@@ -542,9 +572,12 @@ class NativeEditorExpoView(
|
|
|
542
572
|
private fun insertMentionSuggestion(suggestion: NativeMentionSuggestion) {
|
|
543
573
|
val mentions = addons.mentions ?: return
|
|
544
574
|
val queryState = mentionQueryState ?: return
|
|
545
|
-
val attrs =
|
|
546
|
-
if (
|
|
547
|
-
|
|
575
|
+
val attrs = resolvedMentionAttrs(mentions.trigger, suggestion)
|
|
576
|
+
if (mentions.resolveSelectionAttrs) {
|
|
577
|
+
emitMentionSelectRequest(mentions.trigger, suggestion, attrs, queryState)
|
|
578
|
+
lastMentionEventJson = null
|
|
579
|
+
clearMentionQueryState()
|
|
580
|
+
return
|
|
548
581
|
}
|
|
549
582
|
val docJson = JSONObject()
|
|
550
583
|
.put("type", "doc")
|
|
@@ -564,7 +597,7 @@ class NativeEditorExpoView(
|
|
|
564
597
|
docJson.toString()
|
|
565
598
|
)
|
|
566
599
|
richTextView.editorEditText.applyUpdateJSON(updateJson)
|
|
567
|
-
emitMentionSelect(mentions.trigger, suggestion)
|
|
600
|
+
emitMentionSelect(mentions.trigger, suggestion, attrs)
|
|
568
601
|
lastMentionEventJson = null
|
|
569
602
|
clearMentionQueryState()
|
|
570
603
|
}
|
|
@@ -295,6 +295,14 @@ class NativeEditorModule : Module() {
|
|
|
295
295
|
editorDestroy(editorId)
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
+
Function("renderDocumentHtml") { configJson: String, html: String ->
|
|
299
|
+
val editorId = editorCreate(configJson)
|
|
300
|
+
try {
|
|
301
|
+
editorSetHtml(editorId, html)
|
|
302
|
+
} finally {
|
|
303
|
+
editorDestroy(editorId)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
298
306
|
|
|
299
307
|
View(NativeEditorExpoView::class) {
|
|
300
308
|
Events(
|
|
@@ -370,7 +378,7 @@ class NativeEditorModule : Module() {
|
|
|
370
378
|
|
|
371
379
|
View(NativeProseViewerExpoView::class) {
|
|
372
380
|
Name("NativeProseViewer")
|
|
373
|
-
Events("onContentHeightChange", "onPressMention")
|
|
381
|
+
Events("onContentHeightChange", "onPressLink", "onPressMention")
|
|
374
382
|
|
|
375
383
|
Prop("renderJson") { view: NativeProseViewerExpoView, renderJson: String? ->
|
|
376
384
|
view.setRenderJson(renderJson)
|
|
@@ -378,6 +386,12 @@ class NativeEditorModule : Module() {
|
|
|
378
386
|
Prop("themeJson") { view: NativeProseViewerExpoView, themeJson: String? ->
|
|
379
387
|
view.setThemeJson(themeJson)
|
|
380
388
|
}
|
|
389
|
+
Prop("enableLinkTaps") { view: NativeProseViewerExpoView, enableLinkTaps: Boolean? ->
|
|
390
|
+
view.setEnableLinkTaps(enableLinkTaps)
|
|
391
|
+
}
|
|
392
|
+
Prop("interceptLinkTaps") { view: NativeProseViewerExpoView, interceptLinkTaps: Boolean? ->
|
|
393
|
+
view.setInterceptLinkTaps(interceptLinkTaps)
|
|
394
|
+
}
|
|
381
395
|
}
|
|
382
396
|
}
|
|
383
397
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
package com.apollohg.editor
|
|
2
2
|
|
|
3
|
+
import android.content.Intent
|
|
3
4
|
import android.content.Context
|
|
4
5
|
import android.graphics.Color
|
|
6
|
+
import android.net.Uri
|
|
5
7
|
import android.view.MotionEvent
|
|
6
8
|
import android.view.View
|
|
7
9
|
import android.view.ViewGroup
|
|
@@ -17,11 +19,15 @@ class NativeProseViewerExpoView(
|
|
|
17
19
|
private val proseView = EditorEditText(context)
|
|
18
20
|
private val onContentHeightChange by EventDispatcher<Map<String, Any>>()
|
|
19
21
|
@Suppress("unused")
|
|
22
|
+
private val onPressLink by EventDispatcher<Map<String, Any>>()
|
|
23
|
+
@Suppress("unused")
|
|
20
24
|
private val onPressMention by EventDispatcher<Map<String, Any>>()
|
|
21
25
|
|
|
22
26
|
private var lastRenderJson: String? = null
|
|
23
27
|
private var lastThemeJson: String? = null
|
|
24
28
|
private var lastEmittedContentHeight = 0
|
|
29
|
+
private var enableLinkTaps = true
|
|
30
|
+
private var interceptLinkTaps = false
|
|
25
31
|
|
|
26
32
|
init {
|
|
27
33
|
proseView.setBaseStyle(
|
|
@@ -43,14 +49,27 @@ class NativeProseViewerExpoView(
|
|
|
43
49
|
return@setOnTouchListener false
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
proseView.mentionHitAt(event.x, event.y)?.let { mention ->
|
|
53
|
+
onPressMention(mapOf("docPos" to mention.docPos, "label" to mention.label))
|
|
54
|
+
return@setOnTouchListener true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!enableLinkTaps) {
|
|
58
|
+
return@setOnTouchListener false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
val link = proseView.linkHitAt(event.x, event.y) ?: return@setOnTouchListener false
|
|
62
|
+
if (interceptLinkTaps) {
|
|
63
|
+
onPressLink(
|
|
64
|
+
mapOf(
|
|
65
|
+
"href" to link.href,
|
|
66
|
+
"text" to link.text
|
|
67
|
+
)
|
|
51
68
|
)
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
return@setOnTouchListener true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return@setOnTouchListener openLink(link.href)
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
addView(
|
|
@@ -83,6 +102,14 @@ class NativeProseViewerExpoView(
|
|
|
83
102
|
}
|
|
84
103
|
}
|
|
85
104
|
|
|
105
|
+
fun setEnableLinkTaps(enableLinkTaps: Boolean?) {
|
|
106
|
+
this.enableLinkTaps = enableLinkTaps ?: true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fun setInterceptLinkTaps(interceptLinkTaps: Boolean?) {
|
|
110
|
+
this.interceptLinkTaps = interceptLinkTaps ?: false
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
87
114
|
val childWidthSpec = getChildMeasureSpec(
|
|
88
115
|
widthMeasureSpec,
|
|
@@ -154,4 +181,14 @@ class NativeProseViewerExpoView(
|
|
|
154
181
|
|
|
155
182
|
return (resources.displayMetrics.widthPixels - paddingLeft - paddingRight).coerceAtLeast(1)
|
|
156
183
|
}
|
|
184
|
+
|
|
185
|
+
private fun openLink(href: String): Boolean {
|
|
186
|
+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(href)).apply {
|
|
187
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
188
|
+
}
|
|
189
|
+
return runCatching {
|
|
190
|
+
context.startActivity(intent)
|
|
191
|
+
true
|
|
192
|
+
}.getOrDefault(false)
|
|
193
|
+
}
|
|
157
194
|
}
|
|
@@ -752,6 +752,7 @@ class CenteredBulletSpan(
|
|
|
752
752
|
object RenderBridge {
|
|
753
753
|
internal const val NATIVE_BLOCKQUOTE_ANNOTATION = "nativeBlockquote"
|
|
754
754
|
internal const val NATIVE_TOP_LEVEL_CHILD_INDEX_ANNOTATION = "nativeTopLevelChildIndex"
|
|
755
|
+
internal const val NATIVE_LINK_HREF_ANNOTATION = "nativeLinkHref"
|
|
755
756
|
private const val NATIVE_SYNTHETIC_PLACEHOLDER_ANNOTATION = "nativeSyntheticPlaceholder"
|
|
756
757
|
|
|
757
758
|
private data class RenderBuildState(
|
|
@@ -909,6 +910,9 @@ object RenderBridge {
|
|
|
909
910
|
val nodeType = element.optString("nodeType", "")
|
|
910
911
|
val label = element.optString("label", "?")
|
|
911
912
|
val docPos = element.optInt("docPos", 0)
|
|
913
|
+
val mentionTheme = EditorMentionTheme.fromJson(
|
|
914
|
+
element.optJSONObject("mentionTheme")
|
|
915
|
+
)
|
|
912
916
|
appendOpaqueInlineAtom(
|
|
913
917
|
state.result,
|
|
914
918
|
nodeType,
|
|
@@ -919,6 +923,7 @@ object RenderBridge {
|
|
|
919
923
|
state.blockStack,
|
|
920
924
|
state.pendingLeadingMargins,
|
|
921
925
|
theme,
|
|
926
|
+
mentionTheme,
|
|
922
927
|
density
|
|
923
928
|
)
|
|
924
929
|
}
|
|
@@ -1170,6 +1175,15 @@ object RenderBridge {
|
|
|
1170
1175
|
end,
|
|
1171
1176
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1172
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
|
+
}
|
|
1173
1187
|
}
|
|
1174
1188
|
}
|
|
1175
1189
|
}
|
|
@@ -1353,6 +1367,7 @@ object RenderBridge {
|
|
|
1353
1367
|
blockStack: MutableList<BlockContext>,
|
|
1354
1368
|
pendingLeadingMargins: MutableMap<Int, PendingLeadingMargin>,
|
|
1355
1369
|
theme: EditorTheme?,
|
|
1370
|
+
mentionTheme: EditorMentionTheme?,
|
|
1356
1371
|
density: Float
|
|
1357
1372
|
) {
|
|
1358
1373
|
val isMention = nodeType == "mention"
|
|
@@ -1360,8 +1375,13 @@ object RenderBridge {
|
|
|
1360
1375
|
val start = builder.length
|
|
1361
1376
|
builder.append(text)
|
|
1362
1377
|
val end = builder.length
|
|
1378
|
+
val resolvedMentionTheme = if (isMention) {
|
|
1379
|
+
theme?.mentions?.mergedWith(mentionTheme) ?: mentionTheme
|
|
1380
|
+
} else {
|
|
1381
|
+
null
|
|
1382
|
+
}
|
|
1363
1383
|
val inlineTextColor = if (isMention) {
|
|
1364
|
-
|
|
1384
|
+
resolvedMentionTheme?.textColor ?: resolveInlineTextColor(blockStack, textColor, theme)
|
|
1365
1385
|
} else {
|
|
1366
1386
|
resolveInlineTextColor(blockStack, textColor, theme)
|
|
1367
1387
|
}
|
|
@@ -1372,7 +1392,7 @@ object RenderBridge {
|
|
|
1372
1392
|
builder.setSpan(
|
|
1373
1393
|
BackgroundColorSpan(
|
|
1374
1394
|
if (isMention) {
|
|
1375
|
-
|
|
1395
|
+
resolvedMentionTheme?.backgroundColor ?: 0x1f1d4ed8
|
|
1376
1396
|
} else {
|
|
1377
1397
|
0x20000000
|
|
1378
1398
|
}
|
|
@@ -1387,8 +1407,8 @@ object RenderBridge {
|
|
|
1387
1407
|
Annotation("nativeDocPos", docPos.toString()),
|
|
1388
1408
|
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1389
1409
|
)
|
|
1390
|
-
if (isMention && (
|
|
1391
|
-
|
|
1410
|
+
if (isMention && (resolvedMentionTheme?.fontWeight == "bold" ||
|
|
1411
|
+
resolvedMentionTheme?.fontWeight?.toIntOrNull()?.let { it >= 600 } == true)
|
|
1392
1412
|
) {
|
|
1393
1413
|
builder.setSpan(
|
|
1394
1414
|
StyleSpan(Typeface.BOLD),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EditorMentionTheme } from './EditorTheme';
|
|
1
2
|
import { type SchemaDefinition } from './schemas';
|
|
2
3
|
export interface NativeEditorModule {
|
|
3
4
|
editorCreate(configJson: string): number;
|
|
@@ -93,6 +94,7 @@ export interface RenderElement {
|
|
|
93
94
|
docPos?: number;
|
|
94
95
|
label?: string;
|
|
95
96
|
attrs?: Record<string, unknown>;
|
|
97
|
+
mentionTheme?: EditorMentionTheme;
|
|
96
98
|
listContext?: ListContext;
|
|
97
99
|
}
|
|
98
100
|
interface RenderBlocksPatch {
|
|
@@ -194,6 +196,8 @@ export declare class NativeEditorBridge {
|
|
|
194
196
|
toggleMark(markType: string): EditorUpdate | null;
|
|
195
197
|
/** Set a mark with attrs on the current selection. */
|
|
196
198
|
setMark(markType: string, attrs: Record<string, unknown>): EditorUpdate | null;
|
|
199
|
+
/** Set a mark with attrs at an explicit scalar selection. */
|
|
200
|
+
setMarkAtSelectionScalar(scalarAnchor: number, scalarHead: number, markType: string, attrs: Record<string, unknown>): EditorUpdate | null;
|
|
197
201
|
/** Remove a mark from the current selection. */
|
|
198
202
|
unsetMark(markType: string): EditorUpdate | null;
|
|
199
203
|
/** Toggle blockquote wrapping for the current block selection. */
|
|
@@ -202,6 +206,10 @@ export declare class NativeEditorBridge {
|
|
|
202
206
|
toggleHeading(level: number): EditorUpdate | null;
|
|
203
207
|
/** Set the document selection by anchor and head positions. */
|
|
204
208
|
setSelection(anchor: number, head: number): void;
|
|
209
|
+
/** Convert a document position to a scalar position used by native text views. */
|
|
210
|
+
docToScalar(docPos: number): number;
|
|
211
|
+
/** Convert a native scalar position back to a document position. */
|
|
212
|
+
scalarToDoc(scalar: number): number;
|
|
205
213
|
/** Get the current selection from the Rust engine (synchronous native call).
|
|
206
214
|
* Always returns the live selection, not a stale cache. */
|
|
207
215
|
getSelection(): Selection;
|
|
@@ -433,6 +433,12 @@ class NativeEditorBridge {
|
|
|
433
433
|
: getNativeModule().editorSetMark(this._editorId, markType, attrsJson);
|
|
434
434
|
return this.parseAndNoteUpdate(json);
|
|
435
435
|
}
|
|
436
|
+
/** Set a mark with attrs at an explicit scalar selection. */
|
|
437
|
+
setMarkAtSelectionScalar(scalarAnchor, scalarHead, markType, attrs) {
|
|
438
|
+
this.assertNotDestroyed();
|
|
439
|
+
const json = getNativeModule().editorSetMarkAtSelectionScalar(this._editorId, scalarAnchor, scalarHead, markType, JSON.stringify(attrs));
|
|
440
|
+
return this.parseAndNoteUpdate(json);
|
|
441
|
+
}
|
|
436
442
|
/** Remove a mark from the current selection. */
|
|
437
443
|
unsetMark(markType) {
|
|
438
444
|
this.assertNotDestroyed();
|
|
@@ -469,6 +475,16 @@ class NativeEditorBridge {
|
|
|
469
475
|
getNativeModule().editorSetSelection(this._editorId, anchor, head);
|
|
470
476
|
this._lastSelection = { type: 'text', anchor, head };
|
|
471
477
|
}
|
|
478
|
+
/** Convert a document position to a scalar position used by native text views. */
|
|
479
|
+
docToScalar(docPos) {
|
|
480
|
+
this.assertNotDestroyed();
|
|
481
|
+
return getNativeModule().editorDocToScalar(this._editorId, docPos);
|
|
482
|
+
}
|
|
483
|
+
/** Convert a native scalar position back to a document position. */
|
|
484
|
+
scalarToDoc(scalar) {
|
|
485
|
+
this.assertNotDestroyed();
|
|
486
|
+
return getNativeModule().editorScalarToDoc(this._editorId, scalar);
|
|
487
|
+
}
|
|
472
488
|
/** Get the current selection from the Rust engine (synchronous native call).
|
|
473
489
|
* Always returns the live selection, not a stale cache. */
|
|
474
490
|
getSelection() {
|
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
-
import { type EditorTheme } from './EditorTheme';
|
|
2
|
+
import { type EditorMentionTheme, type EditorTheme } from './EditorTheme';
|
|
3
3
|
import type { DocumentJSON } from './NativeEditorBridge';
|
|
4
4
|
import { type SchemaDefinition } from './schemas';
|
|
5
|
-
export interface
|
|
5
|
+
export interface NativeProseViewerMentionRenderContext {
|
|
6
6
|
docPos: number;
|
|
7
7
|
label: string;
|
|
8
8
|
attrs: Record<string, unknown>;
|
|
9
9
|
}
|
|
10
|
+
export interface NativeProseViewerMentionPressEvent extends NativeProseViewerMentionRenderContext {
|
|
11
|
+
}
|
|
12
|
+
export interface NativeProseViewerLinkPressEvent {
|
|
13
|
+
href: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
10
16
|
type NativeProseViewerContent = DocumentJSON | string;
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
interface NativeProseViewerBaseProps {
|
|
18
|
+
contentRevision?: string | number;
|
|
13
19
|
contentJSONRevision?: string | number;
|
|
14
20
|
schema?: SchemaDefinition;
|
|
15
21
|
theme?: EditorTheme;
|
|
16
22
|
style?: StyleProp<ViewStyle>;
|
|
17
23
|
allowBase64Images?: boolean;
|
|
24
|
+
collapseTrailingEmptyParagraphs?: boolean;
|
|
25
|
+
enableLinkTaps?: boolean;
|
|
26
|
+
mentionPrefix?: string | ((mention: NativeProseViewerMentionRenderContext) => string | null | undefined);
|
|
27
|
+
resolveMentionTheme?: (mention: NativeProseViewerMentionRenderContext) => EditorMentionTheme | null | undefined;
|
|
28
|
+
onPressLink?: (event: NativeProseViewerLinkPressEvent) => void;
|
|
18
29
|
onPressMention?: (event: NativeProseViewerMentionPressEvent) => void;
|
|
19
30
|
}
|
|
20
|
-
|
|
31
|
+
interface NativeProseViewerJsonProps extends NativeProseViewerBaseProps {
|
|
32
|
+
contentJSON: NativeProseViewerContent;
|
|
33
|
+
contentHTML?: never;
|
|
34
|
+
}
|
|
35
|
+
interface NativeProseViewerHtmlProps extends NativeProseViewerBaseProps {
|
|
36
|
+
contentHTML: string;
|
|
37
|
+
contentJSON?: never;
|
|
38
|
+
}
|
|
39
|
+
export type NativeProseViewerProps = NativeProseViewerJsonProps | NativeProseViewerHtmlProps;
|
|
40
|
+
export declare function NativeProseViewer({ ...props }: NativeProseViewerProps): import("react/jsx-runtime").JSX.Element;
|
|
21
41
|
export {};
|