@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.
@@ -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.11",
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",