@openeditor/react-native-prose-editor 0.0.10 → 0.0.12
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/NativeBlockEditorSurface.kt +135 -4
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +2 -0
- package/dist/NativeRichTextEditor.d.ts +7 -0
- package/dist/NativeRichTextEditor.js +11 -1
- package/dist/addons.d.ts +6 -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/Generated_editor_core.swift +15 -0
- package/ios/NativeBlockEditorSurface.swift +145 -2
- package/ios/NativeEditorExpoView.swift +8 -0
- package/ios/RichTextEditorView.swift +4 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +11 -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/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +31 -0
|
@@ -42,10 +42,12 @@ import uniffi.editor_core.editorSetNodeSelection
|
|
|
42
42
|
import uniffi.editor_core.editorSetSelection
|
|
43
43
|
import uniffi.editor_core.editorSplitBlock
|
|
44
44
|
import uniffi.editor_core.editorToggleTaskItemCheckedAtSelectionScalar
|
|
45
|
+
import uniffi.editor_core.editorUpdateNodeAttrs
|
|
45
46
|
|
|
46
47
|
internal data class SelectedImageGeometry(val docPos: Int, val rect: RectF)
|
|
47
48
|
|
|
48
49
|
class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
50
|
+
var onPageOpen: ((String, String, String?, String?) -> Unit)? = null
|
|
49
51
|
var isScrollingEnabled: Boolean = true
|
|
50
52
|
|
|
51
53
|
internal var geometryRevision: Long = 0
|
|
@@ -616,6 +618,9 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
616
618
|
internal fun tableFillColorsForTesting(): List<Int> = tableCells.map { it.baseFillColor }
|
|
617
619
|
|
|
618
620
|
private fun viewForNode(node: JSONObject, depth: Int, path: List<Int>): View {
|
|
621
|
+
if (node.optString("nodeType") == "page") {
|
|
622
|
+
return pageView(node, depth).also { nodeViews[path] = it }
|
|
623
|
+
}
|
|
619
624
|
val rendered = when (node.optString("kind")) {
|
|
620
625
|
"list" -> listView(node, depth, path)
|
|
621
626
|
"listItem" -> listItemView(node, 0, null, depth, path)
|
|
@@ -696,10 +701,6 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
696
701
|
orientation = LinearLayout.HORIZONTAL
|
|
697
702
|
isBaselineAligned = false
|
|
698
703
|
}
|
|
699
|
-
row.addView(
|
|
700
|
-
markerView(node, parentListType, index),
|
|
701
|
-
LinearLayout.LayoutParams((24 * density).toInt(), (24 * density).toInt())
|
|
702
|
-
)
|
|
703
704
|
val content = LinearLayout(context).apply {
|
|
704
705
|
orientation = LinearLayout.VERTICAL
|
|
705
706
|
setPadding((8 * density).toInt(), 0, 0, 0)
|
|
@@ -711,12 +712,26 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
711
712
|
if (content.childCount == 0) {
|
|
712
713
|
content.addView(textBlockView(node, depth + 1))
|
|
713
714
|
}
|
|
715
|
+
val marker = if (parentListType == "toggleList" || parentListType == "toggle_list") {
|
|
716
|
+
val open = node.optJSONObject("attrs")?.optBoolean("open", true) ?: true
|
|
717
|
+
setToggleContent(content, open)
|
|
718
|
+
toggleMarkerView(node, content, open)
|
|
719
|
+
} else {
|
|
720
|
+
markerView(node, parentListType, index)
|
|
721
|
+
}
|
|
722
|
+
row.addView(
|
|
723
|
+
marker,
|
|
724
|
+
LinearLayout.LayoutParams((24 * density).toInt(), (24 * density).toInt())
|
|
725
|
+
)
|
|
714
726
|
row.addView(content, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
|
715
727
|
return row
|
|
716
728
|
}
|
|
717
729
|
|
|
718
730
|
private fun blockContainerView(node: JSONObject, depth: Int, path: List<Int>): View {
|
|
719
731
|
val density = resources.displayMetrics.density
|
|
732
|
+
if (node.optString("nodeType") == "callout") {
|
|
733
|
+
return calloutView(node, depth, path)
|
|
734
|
+
}
|
|
720
735
|
if (node.optString("nodeType") == "blockquote") {
|
|
721
736
|
val row = LinearLayout(context).apply {
|
|
722
737
|
orientation = LinearLayout.HORIZONTAL
|
|
@@ -755,6 +770,86 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
755
770
|
return container
|
|
756
771
|
}
|
|
757
772
|
|
|
773
|
+
private fun calloutView(node: JSONObject, depth: Int, path: List<Int>): View {
|
|
774
|
+
val density = resources.displayMetrics.density
|
|
775
|
+
val tone = node.optJSONObject("attrs")?.optString("tone", "info") ?: "info"
|
|
776
|
+
val accent = when (tone) {
|
|
777
|
+
"success" -> 0xFF16A34A.toInt()
|
|
778
|
+
"warning" -> 0xFFD97706.toInt()
|
|
779
|
+
"danger" -> 0xFFDC2626.toInt()
|
|
780
|
+
else -> 0xFF2563EB.toInt()
|
|
781
|
+
}
|
|
782
|
+
val container = LinearLayout(context).apply {
|
|
783
|
+
orientation = LinearLayout.VERTICAL
|
|
784
|
+
val padding = (12 * density).toInt()
|
|
785
|
+
setPadding(padding, padding, padding, padding)
|
|
786
|
+
background = roundedDrawable(
|
|
787
|
+
Color.argb(24, Color.red(accent), Color.green(accent), Color.blue(accent)),
|
|
788
|
+
Color.argb(140, Color.red(accent), Color.green(accent), Color.blue(accent)),
|
|
789
|
+
1f,
|
|
790
|
+
10f
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
container.addView(TextView(context).apply {
|
|
794
|
+
text = tone.replaceFirstChar { it.uppercase() }
|
|
795
|
+
setTextColor(accent)
|
|
796
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
|
797
|
+
setTypeface(typeface, Typeface.BOLD)
|
|
798
|
+
isClickable = this@NativeBlockEditorSurface.isEditable
|
|
799
|
+
setOnClickListener { cycleCalloutTone(node) }
|
|
800
|
+
})
|
|
801
|
+
val children = node.optJSONArray("children")
|
|
802
|
+
for (index in 0 until (children?.length() ?: 0)) {
|
|
803
|
+
children?.optJSONObject(index)?.let { container.addView(viewForNode(it, depth + 1, path + index)) }
|
|
804
|
+
}
|
|
805
|
+
return container
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private fun toggleMarkerView(node: JSONObject, content: LinearLayout, initiallyOpen: Boolean): View =
|
|
809
|
+
TextView(context).apply {
|
|
810
|
+
var open = initiallyOpen
|
|
811
|
+
text = if (open) "⌄" else "›"
|
|
812
|
+
contentDescription = if (open) "Collapse toggle item" else "Expand toggle item"
|
|
813
|
+
gravity = android.view.Gravity.CENTER
|
|
814
|
+
setTextColor(currentTheme?.list?.markerColor ?: baseTextColor)
|
|
815
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
|
|
816
|
+
setTypeface(typeface, Typeface.BOLD)
|
|
817
|
+
setOnClickListener {
|
|
818
|
+
open = !open
|
|
819
|
+
text = if (open) "⌄" else "›"
|
|
820
|
+
contentDescription = if (open) "Collapse toggle item" else "Expand toggle item"
|
|
821
|
+
setToggleContent(content, open)
|
|
822
|
+
if (isEditable) updateNodeAttrs(node, JSONObject().put("open", open))
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private fun setToggleContent(content: LinearLayout, open: Boolean) {
|
|
827
|
+
for (index in 1 until content.childCount) {
|
|
828
|
+
content.getChildAt(index).visibility = if (open) View.VISIBLE else View.GONE
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private fun cycleCalloutTone(node: JSONObject) {
|
|
833
|
+
if (!isEditable) return
|
|
834
|
+
val tones = listOf("info", "success", "warning", "danger")
|
|
835
|
+
val current = node.optJSONObject("attrs")?.optString("tone", "info") ?: "info"
|
|
836
|
+
val next = tones[(tones.indexOf(current).coerceAtLeast(0) + 1) % tones.size]
|
|
837
|
+
updateNodeAttrs(node, JSONObject().put("tone", next))
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private fun updateNodeAttrs(node: JSONObject, patch: JSONObject) {
|
|
841
|
+
if (currentEditorId == 0L) return
|
|
842
|
+
val attrs = JSONObject(node.optJSONObject("attrs")?.toString() ?: "{}")
|
|
843
|
+
patch.keys().forEach { key -> attrs.put(key, patch.get(key)) }
|
|
844
|
+
val updateJSON = editorUpdateNodeAttrs(
|
|
845
|
+
currentEditorId.toULong(),
|
|
846
|
+
node.optInt("docPos", 0).toUInt(),
|
|
847
|
+
attrs.toString()
|
|
848
|
+
)
|
|
849
|
+
applyUpdateJSON(updateJSON, currentEditorId)
|
|
850
|
+
onAppliedUpdate?.invoke(updateJSON)
|
|
851
|
+
}
|
|
852
|
+
|
|
758
853
|
private fun tableView(node: JSONObject, path: List<Int>): View {
|
|
759
854
|
val tableId = nextTableId++
|
|
760
855
|
val table = LinearLayout(context).apply {
|
|
@@ -829,6 +924,42 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
829
924
|
return TextLeaf(context).also { configureTextLeaf(it, node) }
|
|
830
925
|
}
|
|
831
926
|
|
|
927
|
+
private fun pageView(node: JSONObject, depth: Int): View {
|
|
928
|
+
val density = resources.displayMetrics.density
|
|
929
|
+
val attrs = node.optJSONObject("attrs")
|
|
930
|
+
val row = LinearLayout(context).apply {
|
|
931
|
+
orientation = LinearLayout.HORIZONTAL
|
|
932
|
+
gravity = android.view.Gravity.CENTER_VERTICAL
|
|
933
|
+
val horizontal = (6 * density).toInt()
|
|
934
|
+
val vertical = (5 * density).toInt()
|
|
935
|
+
setPadding(horizontal + (depth * 8 * density).toInt(), vertical, horizontal, vertical)
|
|
936
|
+
}
|
|
937
|
+
row.addView(TextView(context).apply {
|
|
938
|
+
text = attrs?.optString("icon", "📄") ?: "📄"
|
|
939
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
|
|
940
|
+
}, LinearLayout.LayoutParams((28 * density).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT))
|
|
941
|
+
val title = textBlockView(node, depth) as TextLeaf
|
|
942
|
+
title.setTypeface(title.typeface, Typeface.BOLD)
|
|
943
|
+
row.addView(title, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
|
944
|
+
row.addView(TextView(context).apply {
|
|
945
|
+
text = "→"
|
|
946
|
+
contentDescription = "Open page"
|
|
947
|
+
gravity = android.view.Gravity.CENTER
|
|
948
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
|
|
949
|
+
setTypeface(typeface, Typeface.BOLD)
|
|
950
|
+
setOnClickListener {
|
|
951
|
+
val pageId = attrs?.optString("pageId").orEmpty()
|
|
952
|
+
if (pageId.isNotEmpty()) onPageOpen?.invoke(
|
|
953
|
+
pageId,
|
|
954
|
+
title.text?.toString().orEmpty().ifEmpty { "Untitled" },
|
|
955
|
+
attrs?.optString("icon")?.takeIf(String::isNotEmpty),
|
|
956
|
+
attrs?.optString("href")?.takeIf(String::isNotEmpty),
|
|
957
|
+
)
|
|
958
|
+
}
|
|
959
|
+
}, LinearLayout.LayoutParams((40 * density).toInt(), (40 * density).toInt()))
|
|
960
|
+
return row
|
|
961
|
+
}
|
|
962
|
+
|
|
832
963
|
private fun configureTextLeaf(leaf: TextLeaf, node: JSONObject) {
|
|
833
964
|
val rendered = attributedInlineContent(node)
|
|
834
965
|
val hadFocus = leaf.hasFocus()
|
|
@@ -727,6 +727,12 @@ class NativeEditorExpoView(
|
|
|
727
727
|
addView(richTextView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
728
728
|
richTextView.onSelectionChange = ::onSelectionChanged
|
|
729
729
|
richTextView.onEditorUpdate = ::onEditorUpdate
|
|
730
|
+
richTextView.onPageOpen = { pageId, title, icon, href ->
|
|
731
|
+
val event = mutableMapOf<String, Any>("type" to "pageOpen", "pageId" to pageId, "title" to title)
|
|
732
|
+
icon?.let { event["icon"] = it }
|
|
733
|
+
href?.let { event["href"] = it }
|
|
734
|
+
emitAddonEvent(mapOf("eventJson" to JSONObject(event).toString()))
|
|
735
|
+
}
|
|
730
736
|
richTextView.onBeforeDetachedFromWindow = {
|
|
731
737
|
prepareForDetachFromWindow()
|
|
732
738
|
}
|
|
@@ -38,6 +38,7 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
38
38
|
var onSelectionChange: ((Int, Int) -> Unit)? = null
|
|
39
39
|
var onEditorUpdate: ((String) -> Unit)? = null
|
|
40
40
|
var onFocusChange: ((Boolean) -> Unit)? = null
|
|
41
|
+
var onPageOpen: ((String, String, String?, String?) -> Unit)? = null
|
|
41
42
|
|
|
42
43
|
var editorId: Long
|
|
43
44
|
get() = currentEditorId
|
|
@@ -60,6 +61,7 @@ class RichTextEditorView @JvmOverloads constructor(
|
|
|
60
61
|
blockSurface.onSelectionChange = { anchor, head -> onSelectionChange?.invoke(anchor, head) }
|
|
61
62
|
blockSurface.onEditorUpdate = { update -> onEditorUpdate?.invoke(update) }
|
|
62
63
|
blockSurface.onFocusChange = { focused -> onFocusChange?.invoke(focused) }
|
|
64
|
+
blockSurface.onPageOpen = { pageId, title, icon, href -> onPageOpen?.invoke(pageId, title, icon, href) }
|
|
63
65
|
updateAppearance()
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -81,6 +81,13 @@ export interface NativeRichTextEditorProps {
|
|
|
81
81
|
onRequestLink?: (context: LinkRequestContext) => void;
|
|
82
82
|
/** Called when a toolbar image item is pressed so the host can choose an image source. */
|
|
83
83
|
onRequestImage?: (context: ImageRequestContext) => void;
|
|
84
|
+
/** Called when the disclosure control on a Page block is pressed. */
|
|
85
|
+
onOpenPage?: (page: {
|
|
86
|
+
pageId: string;
|
|
87
|
+
title: string;
|
|
88
|
+
icon?: string | null;
|
|
89
|
+
href?: string | null;
|
|
90
|
+
}) => void;
|
|
84
91
|
/** Whether plain URLs typed or pasted into the editor should be converted into link marks automatically. */
|
|
85
92
|
autoDetectLinks?: boolean;
|
|
86
93
|
/** Whether `data:image/...` sources are accepted for image insertion and HTML parsing. */
|
|
@@ -559,7 +559,7 @@ function doesLiveMentionQueryConflictWithNativeSelectRequest(request, currentQue
|
|
|
559
559
|
currentQuery.range.anchor !== request.range.anchor ||
|
|
560
560
|
currentQuery.range.head !== request.range.head);
|
|
561
561
|
}
|
|
562
|
-
exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, valueJSONUpdateMode = 'replace', preserveSelectionOnValueJSONReset = false, selectionOnValueJSONReset, schema, placeholder, editable = true, maxLength, autoFocus = false, autoCapitalize, autoCorrect, keyboardType, keyboardAppearance, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, autoDetectLinks = false, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
|
|
562
|
+
exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, valueJSONUpdateMode = 'replace', preserveSelectionOnValueJSONReset = false, selectionOnValueJSONReset, schema, placeholder, editable = true, maxLength, autoFocus = false, autoCapitalize, autoCorrect, keyboardType, keyboardAppearance, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, onOpenPage, autoDetectLinks = false, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
|
|
563
563
|
const bridgeRef = (0, react_1.useRef)(null);
|
|
564
564
|
const nativeViewRef = (0, react_1.useRef)(null);
|
|
565
565
|
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
@@ -2054,6 +2054,15 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
2054
2054
|
}
|
|
2055
2055
|
if (!parsed)
|
|
2056
2056
|
return;
|
|
2057
|
+
if (parsed.type === 'pageOpen') {
|
|
2058
|
+
onOpenPage?.({
|
|
2059
|
+
pageId: parsed.pageId,
|
|
2060
|
+
title: parsed.title,
|
|
2061
|
+
icon: parsed.icon,
|
|
2062
|
+
href: parsed.href,
|
|
2063
|
+
});
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2057
2066
|
let parsedDocumentVersion = typeof parsed.documentVersion === 'number' ? parsed.documentVersion : undefined;
|
|
2058
2067
|
if (typeof parsedDocumentVersion !== 'number' &&
|
|
2059
2068
|
parsed.type === 'mentionsSelectRequest' &&
|
|
@@ -2169,6 +2178,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
2169
2178
|
resolveMentionInsertionAttrs,
|
|
2170
2179
|
runAndApplyWithCommandRetry,
|
|
2171
2180
|
setMentionQueryEventState,
|
|
2181
|
+
onOpenPage,
|
|
2172
2182
|
syncPreflightUpdateFromNativeEvent,
|
|
2173
2183
|
]);
|
|
2174
2184
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
package/dist/addons.d.ts
CHANGED
|
@@ -65,6 +65,12 @@ export interface SerializedEditorAddons {
|
|
|
65
65
|
mentions?: SerializedMentionsAddonConfig;
|
|
66
66
|
}
|
|
67
67
|
export type EditorAddonEvent = {
|
|
68
|
+
type: 'pageOpen';
|
|
69
|
+
pageId: string;
|
|
70
|
+
title: string;
|
|
71
|
+
icon?: string | null;
|
|
72
|
+
href?: string | null;
|
|
73
|
+
} | {
|
|
68
74
|
type: 'mentionsQueryChange';
|
|
69
75
|
query: string;
|
|
70
76
|
trigger: string;
|
|
Binary file
|
|
Binary file
|
|
@@ -1323,6 +1323,18 @@ public func editorUnwrapFromListAtSelectionScalar(id: UInt64, scalarAnchor: UInt
|
|
|
1323
1323
|
)
|
|
1324
1324
|
})
|
|
1325
1325
|
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Replace a node's attributes at its document position.
|
|
1328
|
+
*/
|
|
1329
|
+
public func editorUpdateNodeAttrs(id: UInt64, pos: UInt32, attrsJson: String) -> String {
|
|
1330
|
+
return try! FfiConverterString.lift(try! rustCall() {
|
|
1331
|
+
uniffi_editor_core_fn_func_editor_update_node_attrs(
|
|
1332
|
+
FfiConverterUInt64.lower(id),
|
|
1333
|
+
FfiConverterUInt32.lower(pos),
|
|
1334
|
+
FfiConverterString.lower(attrsJson),$0
|
|
1335
|
+
)
|
|
1336
|
+
})
|
|
1337
|
+
}
|
|
1326
1338
|
/**
|
|
1327
1339
|
* Wrap the current selection in a list. Returns an update JSON string.
|
|
1328
1340
|
*/
|
|
@@ -1582,6 +1594,9 @@ private let initializationResult: InitializationResult = {
|
|
|
1582
1594
|
if (uniffi_editor_core_checksum_func_editor_unwrap_from_list_at_selection_scalar() != 57899) {
|
|
1583
1595
|
return InitializationResult.apiChecksumMismatch
|
|
1584
1596
|
}
|
|
1597
|
+
if (uniffi_editor_core_checksum_func_editor_update_node_attrs() != 14684) {
|
|
1598
|
+
return InitializationResult.apiChecksumMismatch
|
|
1599
|
+
}
|
|
1585
1600
|
if (uniffi_editor_core_checksum_func_editor_wrap_in_list() != 32846) {
|
|
1586
1601
|
return InitializationResult.apiChecksumMismatch
|
|
1587
1602
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
|
|
3
3
|
final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestureRecognizerDelegate {
|
|
4
|
+
var onPageOpen: ((String, String, String?, String?) -> Void)?
|
|
4
5
|
private final class TextLeafView: UITextView {
|
|
5
6
|
struct PositionSegment {
|
|
6
7
|
let range: NSRange
|
|
@@ -696,6 +697,11 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
|
|
|
696
697
|
}
|
|
697
698
|
|
|
698
699
|
private func view(for node: [String: Any], depth: Int, path: [Int]) -> UIView {
|
|
700
|
+
if node["nodeType"] as? String == "page" {
|
|
701
|
+
let rendered = pageView(for: node, depth: depth)
|
|
702
|
+
nodeViews[path] = rendered
|
|
703
|
+
return rendered
|
|
704
|
+
}
|
|
699
705
|
let rendered: UIView = switch node["kind"] as? String {
|
|
700
706
|
case "list":
|
|
701
707
|
listView(for: node, depth: depth, path: path)
|
|
@@ -785,8 +791,6 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
|
|
|
785
791
|
row.alignment = .top
|
|
786
792
|
row.spacing = 8
|
|
787
793
|
|
|
788
|
-
row.addArrangedSubview(markerView(for: node, parentListType: parentListType, index: index))
|
|
789
|
-
|
|
790
794
|
let content = UIStackView()
|
|
791
795
|
content.axis = .vertical
|
|
792
796
|
content.spacing = 4
|
|
@@ -796,11 +800,20 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
|
|
|
796
800
|
if content.arrangedSubviews.isEmpty {
|
|
797
801
|
content.addArrangedSubview(textBlockView(for: node, depth: depth + 1))
|
|
798
802
|
}
|
|
803
|
+
if parentListType == "toggleList" || parentListType == "toggle_list" {
|
|
804
|
+
row.addArrangedSubview(toggleMarkerView(for: node, content: content))
|
|
805
|
+
setToggleContent(content, open: boolAttr(node, "open") ?? true)
|
|
806
|
+
} else {
|
|
807
|
+
row.addArrangedSubview(markerView(for: node, parentListType: parentListType, index: index))
|
|
808
|
+
}
|
|
799
809
|
row.addArrangedSubview(content)
|
|
800
810
|
return row
|
|
801
811
|
}
|
|
802
812
|
|
|
803
813
|
private func blockContainerView(for node: [String: Any], depth: Int, path: [Int]) -> UIView {
|
|
814
|
+
if (node["nodeType"] as? String) == "callout" {
|
|
815
|
+
return calloutView(for: node, depth: depth, path: path)
|
|
816
|
+
}
|
|
804
817
|
let container = UIStackView()
|
|
805
818
|
container.axis = .vertical
|
|
806
819
|
container.spacing = 4
|
|
@@ -834,6 +847,96 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
|
|
|
834
847
|
return container
|
|
835
848
|
}
|
|
836
849
|
|
|
850
|
+
private func calloutView(for node: [String: Any], depth: Int, path: [Int]) -> UIView {
|
|
851
|
+
let tone = stringAttr(node, "tone") ?? "info"
|
|
852
|
+
let accent: UIColor = switch tone {
|
|
853
|
+
case "success": .systemGreen
|
|
854
|
+
case "warning": .systemOrange
|
|
855
|
+
case "danger": .systemRed
|
|
856
|
+
default: .systemBlue
|
|
857
|
+
}
|
|
858
|
+
let container = UIStackView()
|
|
859
|
+
container.axis = .vertical
|
|
860
|
+
container.spacing = 8
|
|
861
|
+
container.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
|
862
|
+
container.isLayoutMarginsRelativeArrangement = true
|
|
863
|
+
container.backgroundColor = accent.withAlphaComponent(0.1)
|
|
864
|
+
container.layer.borderColor = accent.withAlphaComponent(0.55).cgColor
|
|
865
|
+
container.layer.borderWidth = 1
|
|
866
|
+
container.layer.cornerRadius = 10
|
|
867
|
+
|
|
868
|
+
let toneButton = UIButton(type: .system)
|
|
869
|
+
toneButton.setTitle(tone.capitalized, for: .normal)
|
|
870
|
+
toneButton.setTitleColor(accent, for: .normal)
|
|
871
|
+
toneButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold)
|
|
872
|
+
toneButton.contentHorizontalAlignment = .leading
|
|
873
|
+
toneButton.isEnabled = isEditable
|
|
874
|
+
toneButton.addAction(UIAction { [weak self] _ in
|
|
875
|
+
self?.cycleCalloutTone(node)
|
|
876
|
+
}, for: .touchUpInside)
|
|
877
|
+
container.addArrangedSubview(toneButton)
|
|
878
|
+
|
|
879
|
+
for (index, child) in (node["children"] as? [[String: Any]] ?? []).enumerated() {
|
|
880
|
+
container.addArrangedSubview(view(for: child, depth: depth + 1, path: path + [index]))
|
|
881
|
+
}
|
|
882
|
+
return container
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private func toggleMarkerView(for node: [String: Any], content: UIStackView) -> UIView {
|
|
886
|
+
let open = boolAttr(node, "open") ?? true
|
|
887
|
+
let button = UIButton(type: .system)
|
|
888
|
+
button.setTitle(open ? "⌄" : "›", for: .normal)
|
|
889
|
+
button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
|
|
890
|
+
button.accessibilityLabel = open ? "Collapse toggle item" : "Expand toggle item"
|
|
891
|
+
button.addAction(UIAction { [weak self, weak button, weak content] _ in
|
|
892
|
+
guard let self, let button, let content else { return }
|
|
893
|
+
let nextOpen = button.title(for: .normal) != "⌄"
|
|
894
|
+
button.setTitle(nextOpen ? "⌄" : "›", for: .normal)
|
|
895
|
+
button.accessibilityLabel = nextOpen ? "Collapse toggle item" : "Expand toggle item"
|
|
896
|
+
self.setToggleContent(content, open: nextOpen)
|
|
897
|
+
if self.isEditable {
|
|
898
|
+
self.updateNodeAttrs(node, patch: ["open": nextOpen])
|
|
899
|
+
}
|
|
900
|
+
}, for: .touchUpInside)
|
|
901
|
+
let wrapper = UIView()
|
|
902
|
+
wrapper.widthAnchor.constraint(equalToConstant: 24).isActive = true
|
|
903
|
+
wrapper.heightAnchor.constraint(equalToConstant: 24).isActive = true
|
|
904
|
+
button.translatesAutoresizingMaskIntoConstraints = false
|
|
905
|
+
wrapper.addSubview(button)
|
|
906
|
+
NSLayoutConstraint.activate([
|
|
907
|
+
button.centerXAnchor.constraint(equalTo: wrapper.centerXAnchor),
|
|
908
|
+
button.centerYAnchor.constraint(equalTo: wrapper.centerYAnchor),
|
|
909
|
+
])
|
|
910
|
+
return wrapper
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private func setToggleContent(_ content: UIStackView, open: Bool) {
|
|
914
|
+
for (index, child) in content.arrangedSubviews.enumerated() where index > 0 {
|
|
915
|
+
child.isHidden = !open
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private func cycleCalloutTone(_ node: [String: Any]) {
|
|
920
|
+
guard isEditable else { return }
|
|
921
|
+
let tones = ["info", "success", "warning", "danger"]
|
|
922
|
+
let current = stringAttr(node, "tone") ?? "info"
|
|
923
|
+
let next = tones[((tones.firstIndex(of: current) ?? 0) + 1) % tones.count]
|
|
924
|
+
updateNodeAttrs(node, patch: ["tone": next])
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private func updateNodeAttrs(_ node: [String: Any], patch: [String: Any]) {
|
|
928
|
+
guard currentEditorId != 0 else { return }
|
|
929
|
+
var attrs = node["attrs"] as? [String: Any] ?? [:]
|
|
930
|
+
attrs.merge(patch) { _, next in next }
|
|
931
|
+
guard let data = try? JSONSerialization.data(withJSONObject: attrs),
|
|
932
|
+
let json = String(data: data, encoding: .utf8)
|
|
933
|
+
else { return }
|
|
934
|
+
let pos = (node["docPos"] as? NSNumber)?.uint32Value ?? 0
|
|
935
|
+
let updateJSON = editorUpdateNodeAttrs(id: currentEditorId, pos: pos, attrsJson: json)
|
|
936
|
+
applyUpdateJSON(updateJSON, editorId: currentEditorId)
|
|
937
|
+
onAppliedUpdate?(updateJSON)
|
|
938
|
+
}
|
|
939
|
+
|
|
837
940
|
func blockquoteStripeRectsForTesting() -> [CGRect] {
|
|
838
941
|
layoutIfNeeded()
|
|
839
942
|
return descendants(of: self)
|
|
@@ -949,6 +1052,42 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
|
|
|
949
1052
|
return leaf
|
|
950
1053
|
}
|
|
951
1054
|
|
|
1055
|
+
private func pageView(for node: [String: Any], depth: Int) -> UIView {
|
|
1056
|
+
let row = UIStackView()
|
|
1057
|
+
row.axis = .horizontal
|
|
1058
|
+
row.alignment = .center
|
|
1059
|
+
row.spacing = 8
|
|
1060
|
+
row.layoutMargins = UIEdgeInsets(top: 5, left: CGFloat(depth) * 8 + 6, bottom: 5, right: 6)
|
|
1061
|
+
row.isLayoutMarginsRelativeArrangement = true
|
|
1062
|
+
row.layer.cornerRadius = 8
|
|
1063
|
+
|
|
1064
|
+
let icon = UILabel()
|
|
1065
|
+
icon.text = stringAttr(node, "icon") ?? "📄"
|
|
1066
|
+
icon.font = .systemFont(ofSize: 18)
|
|
1067
|
+
icon.setContentHuggingPriority(.required, for: .horizontal)
|
|
1068
|
+
row.addArrangedSubview(icon)
|
|
1069
|
+
|
|
1070
|
+
let title = textBlockView(for: node, depth: depth)
|
|
1071
|
+
if let leaf = title as? UITextView {
|
|
1072
|
+
leaf.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
1073
|
+
leaf.textColor = baseTextColor
|
|
1074
|
+
}
|
|
1075
|
+
row.addArrangedSubview(title)
|
|
1076
|
+
|
|
1077
|
+
let open = UIButton(type: .system)
|
|
1078
|
+
open.setTitle("→", for: .normal)
|
|
1079
|
+
open.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
|
|
1080
|
+
open.accessibilityLabel = "Open page"
|
|
1081
|
+
open.setContentHuggingPriority(.required, for: .horizontal)
|
|
1082
|
+
open.addAction(UIAction { [weak self, weak title] _ in
|
|
1083
|
+
guard let pageId = self?.stringAttr(node, "pageId"), !pageId.isEmpty else { return }
|
|
1084
|
+
let pageTitle = (title as? UITextView)?.text ?? "Untitled"
|
|
1085
|
+
self?.onPageOpen?(pageId, pageTitle, self?.stringAttr(node, "icon"), self?.stringAttr(node, "href"))
|
|
1086
|
+
}, for: .touchUpInside)
|
|
1087
|
+
row.addArrangedSubview(open)
|
|
1088
|
+
return row
|
|
1089
|
+
}
|
|
1090
|
+
|
|
952
1091
|
private func configureTextLeaf(_ leaf: TextLeafView, for node: [String: Any]) {
|
|
953
1092
|
let wasFirstResponder = leaf.isFirstResponder
|
|
954
1093
|
let previousSelection = leaf.selectedRange
|
|
@@ -1549,6 +1688,10 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
|
|
|
1549
1688
|
return attrs[key] as? Bool
|
|
1550
1689
|
}
|
|
1551
1690
|
|
|
1691
|
+
private func stringAttr(_ node: [String: Any], _ key: String) -> String? {
|
|
1692
|
+
(node["attrs"] as? [String: Any])?[key] as? String
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1552
1695
|
private func isCodeBlock(_ node: [String: Any]) -> Bool {
|
|
1553
1696
|
let nodeType = node["nodeType"] as? String
|
|
1554
1697
|
return nodeType == "codeBlock" || nodeType == "code_block"
|
|
@@ -2101,6 +2101,14 @@ class NativeEditorExpoView: ExpoView, UIGestureRecognizerDelegate {
|
|
|
2101
2101
|
if focused { self?.handleTextDidBeginEditing() }
|
|
2102
2102
|
else { self?.handleTextDidEndEditing() }
|
|
2103
2103
|
}
|
|
2104
|
+
richTextView.onPageOpen = { [weak self] pageId, title, icon, href in
|
|
2105
|
+
var event: [String: Any] = ["type": "pageOpen", "pageId": pageId, "title": title]
|
|
2106
|
+
if let icon { event["icon"] = icon }
|
|
2107
|
+
if let href { event["href"] = href }
|
|
2108
|
+
guard let data = try? JSONSerialization.data(withJSONObject: event),
|
|
2109
|
+
let json = String(data: data, encoding: .utf8) else { return }
|
|
2110
|
+
self?.onAddonEvent(["eventJson": json])
|
|
2111
|
+
}
|
|
2104
2112
|
configureAccessoryToolbar()
|
|
2105
2113
|
|
|
2106
2114
|
addSubview(richTextView)
|
|
@@ -646,6 +646,7 @@ final class RichTextEditorView: UIView {
|
|
|
646
646
|
var onSelectionChange: ((UInt32, UInt32) -> Void)?
|
|
647
647
|
var onEditorUpdate: ((String) -> Void)?
|
|
648
648
|
var onFocusChange: ((Bool) -> Void)?
|
|
649
|
+
var onPageOpen: ((String, String, String?, String?) -> Void)?
|
|
649
650
|
private var lastAutoGrowWidth: CGFloat = 0
|
|
650
651
|
private var cachedAutoGrowMeasuredHeight: CGFloat = 0
|
|
651
652
|
private var remoteSelections: [RemoteSelectionDecoration] = []
|
|
@@ -760,6 +761,9 @@ final class RichTextEditorView: UIView {
|
|
|
760
761
|
blockSurface.onFocusChange = { [weak self] focused in
|
|
761
762
|
self?.onFocusChange?(focused)
|
|
762
763
|
}
|
|
764
|
+
blockSurface.onPageOpen = { [weak self] pageId, title, icon, href in
|
|
765
|
+
self?.onPageOpen?(pageId, title, icon, href)
|
|
766
|
+
}
|
|
763
767
|
blockSurface.onContentHeightMayChange = { [weak self] measuredHeight in
|
|
764
768
|
guard let self, self.heightBehavior == .autoGrow else { return }
|
|
765
769
|
self.cachedAutoGrowMeasuredHeight = measuredHeight
|
|
@@ -617,6 +617,11 @@ RustBuffer uniffi_editor_core_fn_func_editor_unwrap_from_list(uint64_t id, RustC
|
|
|
617
617
|
RustBuffer uniffi_editor_core_fn_func_editor_unwrap_from_list_at_selection_scalar(uint64_t id, uint32_t scalar_anchor, uint32_t scalar_head, RustCallStatus *_Nonnull out_status
|
|
618
618
|
);
|
|
619
619
|
#endif
|
|
620
|
+
#ifndef UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_FN_FUNC_EDITOR_UPDATE_NODE_ATTRS
|
|
621
|
+
#define UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_FN_FUNC_EDITOR_UPDATE_NODE_ATTRS
|
|
622
|
+
RustBuffer uniffi_editor_core_fn_func_editor_update_node_attrs(uint64_t id, uint32_t pos, RustBuffer attrs_json, RustCallStatus *_Nonnull out_status
|
|
623
|
+
);
|
|
624
|
+
#endif
|
|
620
625
|
#ifndef UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_FN_FUNC_EDITOR_WRAP_IN_LIST
|
|
621
626
|
#define UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_FN_FUNC_EDITOR_WRAP_IN_LIST
|
|
622
627
|
RustBuffer uniffi_editor_core_fn_func_editor_wrap_in_list(uint64_t id, RustBuffer list_type, RustCallStatus *_Nonnull out_status
|
|
@@ -1343,6 +1348,12 @@ uint16_t uniffi_editor_core_checksum_func_editor_unwrap_from_list(void
|
|
|
1343
1348
|
#define UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_CHECKSUM_FUNC_EDITOR_UNWRAP_FROM_LIST_AT_SELECTION_SCALAR
|
|
1344
1349
|
uint16_t uniffi_editor_core_checksum_func_editor_unwrap_from_list_at_selection_scalar(void
|
|
1345
1350
|
|
|
1351
|
+
);
|
|
1352
|
+
#endif
|
|
1353
|
+
#ifndef UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_CHECKSUM_FUNC_EDITOR_UPDATE_NODE_ATTRS
|
|
1354
|
+
#define UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_CHECKSUM_FUNC_EDITOR_UPDATE_NODE_ATTRS
|
|
1355
|
+
uint16_t uniffi_editor_core_checksum_func_editor_update_node_attrs(void
|
|
1356
|
+
|
|
1346
1357
|
);
|
|
1347
1358
|
#endif
|
|
1348
1359
|
#ifndef UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_CHECKSUM_FUNC_EDITOR_WRAP_IN_LIST
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openeditor/react-native-prose-editor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "OpenEditor-owned native rich text editor engine for React Native",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/EasyLink-HQ/openeditor",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -911,6 +911,8 @@ internal interface IntegrityCheckingUniffiLib : Library {
|
|
|
911
911
|
|
|
912
912
|
fun uniffi_editor_core_checksum_func_editor_unwrap_from_list_at_selection_scalar(): Short
|
|
913
913
|
|
|
914
|
+
fun uniffi_editor_core_checksum_func_editor_update_node_attrs(): Short
|
|
915
|
+
|
|
914
916
|
fun uniffi_editor_core_checksum_func_editor_wrap_in_list(): Short
|
|
915
917
|
|
|
916
918
|
fun uniffi_editor_core_checksum_func_editor_wrap_in_list_at_selection_scalar(): Short
|
|
@@ -1398,6 +1400,13 @@ internal interface UniffiLib : Library {
|
|
|
1398
1400
|
uniffi_out_err: UniffiRustCallStatus,
|
|
1399
1401
|
): RustBuffer.ByValue
|
|
1400
1402
|
|
|
1403
|
+
fun uniffi_editor_core_fn_func_editor_update_node_attrs(
|
|
1404
|
+
`id`: Long,
|
|
1405
|
+
`pos`: Int,
|
|
1406
|
+
`attrsJson`: RustBuffer.ByValue,
|
|
1407
|
+
uniffi_out_err: UniffiRustCallStatus,
|
|
1408
|
+
): RustBuffer.ByValue
|
|
1409
|
+
|
|
1401
1410
|
fun uniffi_editor_core_fn_func_editor_wrap_in_list(
|
|
1402
1411
|
`id`: Long,
|
|
1403
1412
|
`listType`: RustBuffer.ByValue,
|
|
@@ -1860,6 +1869,9 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
|
|
|
1860
1869
|
if (lib.uniffi_editor_core_checksum_func_editor_unwrap_from_list_at_selection_scalar() != 57899.toShort()) {
|
|
1861
1870
|
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
|
1862
1871
|
}
|
|
1872
|
+
if (lib.uniffi_editor_core_checksum_func_editor_update_node_attrs() != 14684.toShort()) {
|
|
1873
|
+
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
|
1874
|
+
}
|
|
1863
1875
|
if (lib.uniffi_editor_core_checksum_func_editor_wrap_in_list() != 32846.toShort()) {
|
|
1864
1876
|
throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
|
|
1865
1877
|
}
|
|
@@ -3212,6 +3224,25 @@ fun `editorUnwrapFromListAtSelectionScalar`(
|
|
|
3212
3224
|
},
|
|
3213
3225
|
)
|
|
3214
3226
|
|
|
3227
|
+
/**
|
|
3228
|
+
* Replace a node's attributes at its document position.
|
|
3229
|
+
*/
|
|
3230
|
+
fun `editorUpdateNodeAttrs`(
|
|
3231
|
+
`id`: kotlin.ULong,
|
|
3232
|
+
`pos`: kotlin.UInt,
|
|
3233
|
+
`attrsJson`: kotlin.String,
|
|
3234
|
+
): kotlin.String =
|
|
3235
|
+
FfiConverterString.lift(
|
|
3236
|
+
uniffiRustCall { _status ->
|
|
3237
|
+
UniffiLib.INSTANCE.uniffi_editor_core_fn_func_editor_update_node_attrs(
|
|
3238
|
+
FfiConverterULong.lower(`id`),
|
|
3239
|
+
FfiConverterUInt.lower(`pos`),
|
|
3240
|
+
FfiConverterString.lower(`attrsJson`),
|
|
3241
|
+
_status,
|
|
3242
|
+
)
|
|
3243
|
+
},
|
|
3244
|
+
)
|
|
3245
|
+
|
|
3215
3246
|
/**
|
|
3216
3247
|
* Wrap the current selection in a list. Returns an update JSON string.
|
|
3217
3248
|
*/
|