@openeditor/react-native-prose-editor 0.0.12 → 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.
@@ -48,6 +48,7 @@ internal data class SelectedImageGeometry(val docPos: Int, val rect: RectF)
48
48
 
49
49
  class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
50
50
  var onPageOpen: ((String, String, String?, String?) -> Unit)? = null
51
+ var onAttachmentOpen: ((String?, String, String?, Long?, String?) -> Unit)? = null
51
52
  var isScrollingEnabled: Boolean = true
52
53
 
53
54
  internal var geometryRevision: Long = 0
@@ -618,6 +619,9 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
618
619
  internal fun tableFillColorsForTesting(): List<Int> = tableCells.map { it.baseFillColor }
619
620
 
620
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
+ }
621
625
  if (node.optString("nodeType") == "page") {
622
626
  return pageView(node, depth).also { nodeViews[path] = it }
623
627
  }
@@ -960,6 +964,36 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
960
964
  return row
961
965
  }
962
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
+
963
997
  private fun configureTextLeaf(leaf: TextLeaf, node: JSONObject) {
964
998
  val rendered = attributedInlineContent(node)
965
999
  val hadFocus = leaf.hasFocus()
@@ -733,6 +733,14 @@ class NativeEditorExpoView(
733
733
  href?.let { event["href"] = it }
734
734
  emitAddonEvent(mapOf("eventJson" to JSONObject(event).toString()))
735
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
+ }
736
744
  richTextView.onBeforeDetachedFromWindow = {
737
745
  prepareForDetachFromWindow()
738
746
  }
@@ -39,6 +39,7 @@ class RichTextEditorView @JvmOverloads constructor(
39
39
  var onEditorUpdate: ((String) -> Unit)? = null
40
40
  var onFocusChange: ((Boolean) -> Unit)? = null
41
41
  var onPageOpen: ((String, String, String?, String?) -> Unit)? = null
42
+ var onAttachmentOpen: ((String?, String, String?, Long?, String?) -> Unit)? = null
42
43
 
43
44
  var editorId: Long
44
45
  get() = currentEditorId
@@ -62,6 +63,7 @@ class RichTextEditorView @JvmOverloads constructor(
62
63
  blockSurface.onEditorUpdate = { update -> onEditorUpdate?.invoke(update) }
63
64
  blockSurface.onFocusChange = { focused -> onFocusChange?.invoke(focused) }
64
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) }
65
67
  updateAppearance()
66
68
  }
67
69
 
@@ -88,6 +88,14 @@ export interface NativeRichTextEditorProps {
88
88
  icon?: string | null;
89
89
  href?: string | null;
90
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;
91
99
  /** Whether plain URLs typed or pasted into the editor should be converted into link marks automatically. */
92
100
  autoDetectLinks?: boolean;
93
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, onOpenPage, 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,16 @@ 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
+ }
2057
2067
  if (parsed.type === 'pageOpen') {
2058
2068
  onOpenPage?.({
2059
2069
  pageId: parsed.pageId,
package/dist/addons.d.ts CHANGED
@@ -65,6 +65,13 @@ 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
+ } | {
68
75
  type: 'pageOpen';
69
76
  pageId: string;
70
77
  title: string;
@@ -2,6 +2,17 @@ import UIKit
2
2
 
3
3
  final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestureRecognizerDelegate {
4
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
+ }
5
16
  private final class TextLeafView: UITextView {
6
17
  struct PositionSegment {
7
18
  let range: NSRange
@@ -697,6 +708,11 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
697
708
  }
698
709
 
699
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
+ }
700
716
  if node["nodeType"] as? String == "page" {
701
717
  let rendered = pageView(for: node, depth: depth)
702
718
  nodeViews[path] = rendered
@@ -1088,6 +1104,49 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
1088
1104
  return row
1089
1105
  }
1090
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
+
1091
1150
  private func configureTextLeaf(_ leaf: TextLeafView, for node: [String: Any]) {
1092
1151
  let wasFirstResponder = leaf.isFirstResponder
1093
1152
  let previousSelection = leaf.selectedRange
@@ -2109,6 +2109,16 @@ class NativeEditorExpoView: ExpoView, UIGestureRecognizerDelegate {
2109
2109
  let json = String(data: data, encoding: .utf8) else { return }
2110
2110
  self?.onAddonEvent(["eventJson": json])
2111
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
+ }
2112
2122
  configureAccessoryToolbar()
2113
2123
 
2114
2124
  addSubview(richTextView)
@@ -647,6 +647,7 @@ final class RichTextEditorView: UIView {
647
647
  var onEditorUpdate: ((String) -> Void)?
648
648
  var onFocusChange: ((Bool) -> Void)?
649
649
  var onPageOpen: ((String, String, String?, String?) -> Void)?
650
+ var onAttachmentOpen: ((String?, String, String?, NSNumber?, String?) -> Void)?
650
651
  private var lastAutoGrowWidth: CGFloat = 0
651
652
  private var cachedAutoGrowMeasuredHeight: CGFloat = 0
652
653
  private var remoteSelections: [RemoteSelectionDecoration] = []
@@ -764,6 +765,9 @@ final class RichTextEditorView: UIView {
764
765
  blockSurface.onPageOpen = { [weak self] pageId, title, icon, href in
765
766
  self?.onPageOpen?(pageId, title, icon, href)
766
767
  }
768
+ blockSurface.onAttachmentOpen = { [weak self] attachmentId, name, mimeType, size, url in
769
+ self?.onAttachmentOpen?(attachmentId, name, mimeType, size, url)
770
+ }
767
771
  blockSurface.onContentHeightMayChange = { [weak self] measuredHeight in
768
772
  guard let self, self.heightBehavior == .autoGrow else { return }
769
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.12",
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",