@openeditor/react-native-prose-editor 0.0.11 → 0.0.13

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,8 @@ 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
51
+ var onAttachmentOpen: ((String?, String, String?, Long?, String?) -> Unit)? = null
50
52
  var isScrollingEnabled: Boolean = true
51
53
 
52
54
  internal var geometryRevision: Long = 0
@@ -617,6 +619,12 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
617
619
  internal fun tableFillColorsForTesting(): List<Int> = tableCells.map { it.baseFillColor }
618
620
 
619
621
  private fun viewForNode(node: JSONObject, depth: Int, path: List<Int>): View {
622
+ if (node.optString("nodeType") == "attachment") {
623
+ return attachmentView(node, depth).also { nodeViews[path] = it }
624
+ }
625
+ if (node.optString("nodeType") == "page") {
626
+ return pageView(node, depth).also { nodeViews[path] = it }
627
+ }
620
628
  val rendered = when (node.optString("kind")) {
621
629
  "list" -> listView(node, depth, path)
622
630
  "listItem" -> listItemView(node, 0, null, depth, path)
@@ -920,6 +928,72 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
920
928
  return TextLeaf(context).also { configureTextLeaf(it, node) }
921
929
  }
922
930
 
931
+ private fun pageView(node: JSONObject, depth: Int): View {
932
+ val density = resources.displayMetrics.density
933
+ val attrs = node.optJSONObject("attrs")
934
+ val row = LinearLayout(context).apply {
935
+ orientation = LinearLayout.HORIZONTAL
936
+ gravity = android.view.Gravity.CENTER_VERTICAL
937
+ val horizontal = (6 * density).toInt()
938
+ val vertical = (5 * density).toInt()
939
+ setPadding(horizontal + (depth * 8 * density).toInt(), vertical, horizontal, vertical)
940
+ }
941
+ row.addView(TextView(context).apply {
942
+ text = attrs?.optString("icon", "📄") ?: "📄"
943
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
944
+ }, LinearLayout.LayoutParams((28 * density).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT))
945
+ val title = textBlockView(node, depth) as TextLeaf
946
+ title.setTypeface(title.typeface, Typeface.BOLD)
947
+ row.addView(title, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
948
+ row.addView(TextView(context).apply {
949
+ text = "→"
950
+ contentDescription = "Open page"
951
+ gravity = android.view.Gravity.CENTER
952
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
953
+ setTypeface(typeface, Typeface.BOLD)
954
+ setOnClickListener {
955
+ val pageId = attrs?.optString("pageId").orEmpty()
956
+ if (pageId.isNotEmpty()) onPageOpen?.invoke(
957
+ pageId,
958
+ title.text?.toString().orEmpty().ifEmpty { "Untitled" },
959
+ attrs?.optString("icon")?.takeIf(String::isNotEmpty),
960
+ attrs?.optString("href")?.takeIf(String::isNotEmpty),
961
+ )
962
+ }
963
+ }, LinearLayout.LayoutParams((40 * density).toInt(), (40 * density).toInt()))
964
+ return row
965
+ }
966
+
967
+ private fun attachmentView(node: JSONObject, depth: Int): View {
968
+ val density = resources.displayMetrics.density
969
+ val attrs = node.optJSONObject("attrs")
970
+ return LinearLayout(context).apply {
971
+ orientation = LinearLayout.HORIZONTAL
972
+ gravity = android.view.Gravity.CENTER_VERTICAL
973
+ val padding = (10 * density).toInt()
974
+ setPadding(padding + (depth * 8 * density).toInt(), padding, padding, padding)
975
+ background = borderDrawable(Color.TRANSPARENT, currentTheme?.table?.borderColor ?: baseTextColor, 1f)
976
+ addView(TextView(context).apply { text = "📎"; setTextSize(TypedValue.COMPLEX_UNIT_SP, 22f) }, LinearLayout.LayoutParams((38 * density).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT))
977
+ addView(LinearLayout(context).apply {
978
+ orientation = LinearLayout.VERTICAL
979
+ addView(TextView(context).apply { text = attrs?.optString("name").orEmpty().ifEmpty { "Attachment" }; setTextColor(baseTextColor); setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f); setTypeface(typeface, Typeface.BOLD) })
980
+ addView(TextView(context).apply { text = attrs?.optString("mimeType").orEmpty().ifEmpty { "File" }; setTextColor(currentTheme?.placeholderColor ?: baseTextColor); setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) })
981
+ }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
982
+ isClickable = true
983
+ isFocusable = true
984
+ contentDescription = "Open ${attrs?.optString("name").orEmpty().ifEmpty { "attachment" }}"
985
+ setOnClickListener {
986
+ onAttachmentOpen?.invoke(
987
+ attrs?.optString("attachmentId")?.takeIf(String::isNotEmpty),
988
+ attrs?.optString("name").orEmpty().ifEmpty { "Attachment" },
989
+ attrs?.optString("mimeType")?.takeIf(String::isNotEmpty),
990
+ attrs?.optLong("size")?.takeIf { attrs.has("size") && !attrs.isNull("size") },
991
+ attrs?.optString("url")?.takeIf(String::isNotEmpty),
992
+ )
993
+ }
994
+ }
995
+ }
996
+
923
997
  private fun configureTextLeaf(leaf: TextLeaf, node: JSONObject) {
924
998
  val rendered = attributedInlineContent(node)
925
999
  val hadFocus = leaf.hasFocus()
@@ -727,6 +727,20 @@ 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
+ }
736
+ richTextView.onAttachmentOpen = { attachmentId, name, mimeType, size, url ->
737
+ val event = mutableMapOf<String, Any>("type" to "attachmentOpen", "name" to name)
738
+ attachmentId?.let { event["attachmentId"] = it }
739
+ mimeType?.let { event["mimeType"] = it }
740
+ size?.let { event["size"] = it }
741
+ url?.let { event["url"] = it }
742
+ emitAddonEvent(mapOf("eventJson" to JSONObject(event).toString()))
743
+ }
730
744
  richTextView.onBeforeDetachedFromWindow = {
731
745
  prepareForDetachFromWindow()
732
746
  }
@@ -38,6 +38,8 @@ 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
42
+ var onAttachmentOpen: ((String?, String, String?, Long?, String?) -> Unit)? = null
41
43
 
42
44
  var editorId: Long
43
45
  get() = currentEditorId
@@ -60,6 +62,8 @@ class RichTextEditorView @JvmOverloads constructor(
60
62
  blockSurface.onSelectionChange = { anchor, head -> onSelectionChange?.invoke(anchor, head) }
61
63
  blockSurface.onEditorUpdate = { update -> onEditorUpdate?.invoke(update) }
62
64
  blockSurface.onFocusChange = { focused -> onFocusChange?.invoke(focused) }
65
+ blockSurface.onPageOpen = { pageId, title, icon, href -> onPageOpen?.invoke(pageId, title, icon, href) }
66
+ blockSurface.onAttachmentOpen = { attachmentId, name, mimeType, size, url -> onAttachmentOpen?.invoke(attachmentId, name, mimeType, size, url) }
63
67
  updateAppearance()
64
68
  }
65
69
 
@@ -81,6 +81,21 @@ 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;
91
+ /** Called when an Attachment block is pressed. */
92
+ onOpenAttachment?: (attachment: {
93
+ attachmentId: string | null;
94
+ name: string;
95
+ mimeType: string | null;
96
+ size: number | null;
97
+ url: string | null;
98
+ }) => void;
84
99
  /** Whether plain URLs typed or pasted into the editor should be converted into link marks automatically. */
85
100
  autoDetectLinks?: boolean;
86
101
  /** 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, onOpenAttachment, 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,25 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
2054
2054
  }
2055
2055
  if (!parsed)
2056
2056
  return;
2057
+ if (parsed.type === 'attachmentOpen') {
2058
+ onOpenAttachment?.({
2059
+ attachmentId: parsed.attachmentId ?? null,
2060
+ name: parsed.name,
2061
+ mimeType: parsed.mimeType ?? null,
2062
+ size: parsed.size ?? null,
2063
+ url: parsed.url ?? null,
2064
+ });
2065
+ return;
2066
+ }
2067
+ if (parsed.type === 'pageOpen') {
2068
+ onOpenPage?.({
2069
+ pageId: parsed.pageId,
2070
+ title: parsed.title,
2071
+ icon: parsed.icon,
2072
+ href: parsed.href,
2073
+ });
2074
+ return;
2075
+ }
2057
2076
  let parsedDocumentVersion = typeof parsed.documentVersion === 'number' ? parsed.documentVersion : undefined;
2058
2077
  if (typeof parsedDocumentVersion !== 'number' &&
2059
2078
  parsed.type === 'mentionsSelectRequest' &&
@@ -2169,6 +2188,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
2169
2188
  resolveMentionInsertionAttrs,
2170
2189
  runAndApplyWithCommandRetry,
2171
2190
  setMentionQueryEventState,
2191
+ onOpenPage,
2172
2192
  syncPreflightUpdateFromNativeEvent,
2173
2193
  ]);
2174
2194
  (0, react_1.useImperativeHandle)(ref, () => ({
package/dist/addons.d.ts CHANGED
@@ -65,6 +65,19 @@ export interface SerializedEditorAddons {
65
65
  mentions?: SerializedMentionsAddonConfig;
66
66
  }
67
67
  export type EditorAddonEvent = {
68
+ type: 'attachmentOpen';
69
+ attachmentId?: string | null;
70
+ name: string;
71
+ mimeType?: string | null;
72
+ size?: number | null;
73
+ url?: string | null;
74
+ } | {
75
+ type: 'pageOpen';
76
+ pageId: string;
77
+ title: string;
78
+ icon?: string | null;
79
+ href?: string | null;
80
+ } | {
68
81
  type: 'mentionsQueryChange';
69
82
  query: string;
70
83
  trigger: string;
@@ -1,6 +1,18 @@
1
1
  import UIKit
2
2
 
3
3
  final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestureRecognizerDelegate {
4
+ var onPageOpen: ((String, String, String?, String?) -> Void)?
5
+ var onAttachmentOpen: ((String?, String, String?, NSNumber?, String?) -> Void)?
6
+ private final class AttachmentRow: UIStackView {
7
+ var open: (() -> Void)?
8
+ override init(frame: CGRect) {
9
+ super.init(frame: frame)
10
+ isUserInteractionEnabled = true
11
+ addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleOpen)))
12
+ }
13
+ required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
14
+ @objc private func handleOpen() { open?() }
15
+ }
4
16
  private final class TextLeafView: UITextView {
5
17
  struct PositionSegment {
6
18
  let range: NSRange
@@ -696,6 +708,16 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
696
708
  }
697
709
 
698
710
  private func view(for node: [String: Any], depth: Int, path: [Int]) -> UIView {
711
+ if node["nodeType"] as? String == "attachment" {
712
+ let rendered = attachmentView(for: node, depth: depth)
713
+ nodeViews[path] = rendered
714
+ return rendered
715
+ }
716
+ if node["nodeType"] as? String == "page" {
717
+ let rendered = pageView(for: node, depth: depth)
718
+ nodeViews[path] = rendered
719
+ return rendered
720
+ }
699
721
  let rendered: UIView = switch node["kind"] as? String {
700
722
  case "list":
701
723
  listView(for: node, depth: depth, path: path)
@@ -1046,6 +1068,85 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
1046
1068
  return leaf
1047
1069
  }
1048
1070
 
1071
+ private func pageView(for node: [String: Any], depth: Int) -> UIView {
1072
+ let row = UIStackView()
1073
+ row.axis = .horizontal
1074
+ row.alignment = .center
1075
+ row.spacing = 8
1076
+ row.layoutMargins = UIEdgeInsets(top: 5, left: CGFloat(depth) * 8 + 6, bottom: 5, right: 6)
1077
+ row.isLayoutMarginsRelativeArrangement = true
1078
+ row.layer.cornerRadius = 8
1079
+
1080
+ let icon = UILabel()
1081
+ icon.text = stringAttr(node, "icon") ?? "📄"
1082
+ icon.font = .systemFont(ofSize: 18)
1083
+ icon.setContentHuggingPriority(.required, for: .horizontal)
1084
+ row.addArrangedSubview(icon)
1085
+
1086
+ let title = textBlockView(for: node, depth: depth)
1087
+ if let leaf = title as? UITextView {
1088
+ leaf.font = .systemFont(ofSize: 16, weight: .semibold)
1089
+ leaf.textColor = baseTextColor
1090
+ }
1091
+ row.addArrangedSubview(title)
1092
+
1093
+ let open = UIButton(type: .system)
1094
+ open.setTitle("→", for: .normal)
1095
+ open.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
1096
+ open.accessibilityLabel = "Open page"
1097
+ open.setContentHuggingPriority(.required, for: .horizontal)
1098
+ open.addAction(UIAction { [weak self, weak title] _ in
1099
+ guard let pageId = self?.stringAttr(node, "pageId"), !pageId.isEmpty else { return }
1100
+ let pageTitle = (title as? UITextView)?.text ?? "Untitled"
1101
+ self?.onPageOpen?(pageId, pageTitle, self?.stringAttr(node, "icon"), self?.stringAttr(node, "href"))
1102
+ }, for: .touchUpInside)
1103
+ row.addArrangedSubview(open)
1104
+ return row
1105
+ }
1106
+
1107
+ private func attachmentView(for node: [String: Any], depth: Int) -> UIView {
1108
+ let row = AttachmentRow()
1109
+ row.axis = .horizontal
1110
+ row.alignment = .center
1111
+ row.spacing = 10
1112
+ row.layoutMargins = UIEdgeInsets(top: 10, left: CGFloat(depth) * 8 + 10, bottom: 10, right: 10)
1113
+ row.isLayoutMarginsRelativeArrangement = true
1114
+ row.layer.cornerRadius = 10
1115
+ row.layer.borderWidth = 1
1116
+ row.layer.borderColor = UIColor.separator.cgColor
1117
+ let icon = UILabel()
1118
+ icon.text = "📎"
1119
+ icon.font = .systemFont(ofSize: 22)
1120
+ row.addArrangedSubview(icon)
1121
+ let details = UIStackView()
1122
+ details.axis = .vertical
1123
+ details.spacing = 2
1124
+ let name = UILabel()
1125
+ name.text = stringAttr(node, "name") ?? "Attachment"
1126
+ name.font = .systemFont(ofSize: 15, weight: .semibold)
1127
+ name.textColor = baseTextColor
1128
+ name.lineBreakMode = .byTruncatingMiddle
1129
+ let meta = UILabel()
1130
+ meta.text = stringAttr(node, "mimeType") ?? "File"
1131
+ meta.font = .systemFont(ofSize: 12)
1132
+ meta.textColor = .secondaryLabel
1133
+ details.addArrangedSubview(name)
1134
+ details.addArrangedSubview(meta)
1135
+ row.addArrangedSubview(details)
1136
+ row.accessibilityTraits = .button
1137
+ row.accessibilityLabel = "Open \(name.text ?? "attachment")"
1138
+ row.open = { [weak self] in
1139
+ self?.onAttachmentOpen?(
1140
+ self?.stringAttr(node, "attachmentId"),
1141
+ self?.stringAttr(node, "name") ?? "Attachment",
1142
+ self?.stringAttr(node, "mimeType"),
1143
+ (node["attrs"] as? [String: Any])?["size"] as? NSNumber,
1144
+ self?.stringAttr(node, "url")
1145
+ )
1146
+ }
1147
+ return row
1148
+ }
1149
+
1049
1150
  private func configureTextLeaf(_ leaf: TextLeafView, for node: [String: Any]) {
1050
1151
  let wasFirstResponder = leaf.isFirstResponder
1051
1152
  let previousSelection = leaf.selectedRange
@@ -2101,6 +2101,24 @@ 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
+ }
2112
+ richTextView.onAttachmentOpen = { [weak self] attachmentId, name, mimeType, size, url in
2113
+ var event: [String: Any] = ["type": "attachmentOpen", "name": name]
2114
+ if let attachmentId { event["attachmentId"] = attachmentId }
2115
+ if let mimeType { event["mimeType"] = mimeType }
2116
+ if let size { event["size"] = size }
2117
+ if let url { event["url"] = url }
2118
+ guard let data = try? JSONSerialization.data(withJSONObject: event),
2119
+ let json = String(data: data, encoding: .utf8) else { return }
2120
+ self?.onAddonEvent(["eventJson": json])
2121
+ }
2104
2122
  configureAccessoryToolbar()
2105
2123
 
2106
2124
  addSubview(richTextView)
@@ -646,6 +646,8 @@ 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)?
650
+ var onAttachmentOpen: ((String?, String, String?, NSNumber?, String?) -> Void)?
649
651
  private var lastAutoGrowWidth: CGFloat = 0
650
652
  private var cachedAutoGrowMeasuredHeight: CGFloat = 0
651
653
  private var remoteSelections: [RemoteSelectionDecoration] = []
@@ -760,6 +762,12 @@ final class RichTextEditorView: UIView {
760
762
  blockSurface.onFocusChange = { [weak self] focused in
761
763
  self?.onFocusChange?(focused)
762
764
  }
765
+ blockSurface.onPageOpen = { [weak self] pageId, title, icon, href in
766
+ self?.onPageOpen?(pageId, title, icon, href)
767
+ }
768
+ blockSurface.onAttachmentOpen = { [weak self] attachmentId, name, mimeType, size, url in
769
+ self?.onAttachmentOpen?(attachmentId, name, mimeType, size, url)
770
+ }
763
771
  blockSurface.onContentHeightMayChange = { [weak self] measuredHeight in
764
772
  guard let self, self.heightBehavior == .autoGrow else { return }
765
773
  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.13",
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",