@openeditor/react-native-prose-editor 0.0.11 → 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 +40 -0
- 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/NativeBlockEditorSurface.swift +42 -0
- package/ios/NativeEditorExpoView.swift +8 -0
- package/ios/RichTextEditorView.swift +4 -0
- package/package.json +1 -1
|
@@ -47,6 +47,7 @@ import uniffi.editor_core.editorUpdateNodeAttrs
|
|
|
47
47
|
internal data class SelectedImageGeometry(val docPos: Int, val rect: RectF)
|
|
48
48
|
|
|
49
49
|
class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
50
|
+
var onPageOpen: ((String, String, String?, String?) -> Unit)? = null
|
|
50
51
|
var isScrollingEnabled: Boolean = true
|
|
51
52
|
|
|
52
53
|
internal var geometryRevision: Long = 0
|
|
@@ -617,6 +618,9 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
617
618
|
internal fun tableFillColorsForTesting(): List<Int> = tableCells.map { it.baseFillColor }
|
|
618
619
|
|
|
619
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
|
+
}
|
|
620
624
|
val rendered = when (node.optString("kind")) {
|
|
621
625
|
"list" -> listView(node, depth, path)
|
|
622
626
|
"listItem" -> listItemView(node, 0, null, depth, path)
|
|
@@ -920,6 +924,42 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
|
|
|
920
924
|
return TextLeaf(context).also { configureTextLeaf(it, node) }
|
|
921
925
|
}
|
|
922
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
|
+
|
|
923
963
|
private fun configureTextLeaf(leaf: TextLeaf, node: JSONObject) {
|
|
924
964
|
val rendered = attributedInlineContent(node)
|
|
925
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;
|
|
@@ -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)
|
|
@@ -1046,6 +1052,42 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
|
|
|
1046
1052
|
return leaf
|
|
1047
1053
|
}
|
|
1048
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
|
+
|
|
1049
1091
|
private func configureTextLeaf(_ leaf: TextLeafView, for node: [String: Any]) {
|
|
1050
1092
|
let wasFirstResponder = leaf.isFirstResponder
|
|
1051
1093
|
let previousSelection = leaf.selectedRange
|
|
@@ -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
|
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",
|