@openeditor/react-native-prose-editor 0.0.12 → 0.0.14

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
  }
@@ -772,36 +776,31 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
772
776
 
773
777
  private fun calloutView(node: JSONObject, depth: Int, path: List<Int>): View {
774
778
  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
779
  val container = LinearLayout(context).apply {
783
- orientation = LinearLayout.VERTICAL
780
+ orientation = LinearLayout.HORIZONTAL
781
+ gravity = android.view.Gravity.TOP
784
782
  val padding = (12 * density).toInt()
785
783
  setPadding(padding, padding, padding, padding)
786
784
  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,
785
+ currentTheme?.codeBlock?.backgroundColor ?: 0xFFF5F5F5.toInt(),
786
+ Color.TRANSPARENT,
787
+ 0f,
790
788
  10f
791
789
  )
792
790
  }
793
791
  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) }
792
+ text = node.optJSONObject("attrs")?.optString("emoji", "💡") ?: "💡"
793
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
800
794
  })
795
+ val content = LinearLayout(context).apply {
796
+ orientation = LinearLayout.VERTICAL
797
+ setPadding((10 * density).toInt(), 0, 0, 0)
798
+ }
801
799
  val children = node.optJSONArray("children")
802
800
  for (index in 0 until (children?.length() ?: 0)) {
803
- children?.optJSONObject(index)?.let { container.addView(viewForNode(it, depth + 1, path + index)) }
801
+ children?.optJSONObject(index)?.let { content.addView(viewForNode(it, depth + 1, path + index)) }
804
802
  }
803
+ container.addView(content, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
805
804
  return container
806
805
  }
807
806
 
@@ -829,14 +828,6 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
829
828
  }
830
829
  }
831
830
 
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
831
  private fun updateNodeAttrs(node: JSONObject, patch: JSONObject) {
841
832
  if (currentEditorId == 0L) return
842
833
  val attrs = JSONObject(node.optJSONObject("attrs")?.toString() ?: "{}")
@@ -960,6 +951,36 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
960
951
  return row
961
952
  }
962
953
 
954
+ private fun attachmentView(node: JSONObject, depth: Int): View {
955
+ val density = resources.displayMetrics.density
956
+ val attrs = node.optJSONObject("attrs")
957
+ return LinearLayout(context).apply {
958
+ orientation = LinearLayout.HORIZONTAL
959
+ gravity = android.view.Gravity.CENTER_VERTICAL
960
+ val padding = (10 * density).toInt()
961
+ setPadding(padding + (depth * 8 * density).toInt(), padding, padding, padding)
962
+ background = borderDrawable(Color.TRANSPARENT, currentTheme?.table?.borderColor ?: baseTextColor, 1f)
963
+ addView(TextView(context).apply { text = "📎"; setTextSize(TypedValue.COMPLEX_UNIT_SP, 22f) }, LinearLayout.LayoutParams((38 * density).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT))
964
+ addView(LinearLayout(context).apply {
965
+ orientation = LinearLayout.VERTICAL
966
+ addView(TextView(context).apply { text = attrs?.optString("name").orEmpty().ifEmpty { "Attachment" }; setTextColor(baseTextColor); setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f); setTypeface(typeface, Typeface.BOLD) })
967
+ addView(TextView(context).apply { text = attrs?.optString("mimeType").orEmpty().ifEmpty { "File" }; setTextColor(currentTheme?.placeholderColor ?: baseTextColor); setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) })
968
+ }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
969
+ isClickable = true
970
+ isFocusable = true
971
+ contentDescription = "Open ${attrs?.optString("name").orEmpty().ifEmpty { "attachment" }}"
972
+ setOnClickListener {
973
+ onAttachmentOpen?.invoke(
974
+ attrs?.optString("attachmentId")?.takeIf(String::isNotEmpty),
975
+ attrs?.optString("name").orEmpty().ifEmpty { "Attachment" },
976
+ attrs?.optString("mimeType")?.takeIf(String::isNotEmpty),
977
+ attrs?.optLong("size")?.takeIf { attrs.has("size") && !attrs.isNull("size") },
978
+ attrs?.optString("url")?.takeIf(String::isNotEmpty),
979
+ )
980
+ }
981
+ }
982
+ }
983
+
963
984
  private fun configureTextLeaf(leaf: TextLeaf, node: JSONObject) {
964
985
  val rendered = attributedInlineContent(node)
965
986
  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
@@ -848,37 +864,27 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
848
864
  }
849
865
 
850
866
  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
867
  let container = UIStackView()
859
- container.axis = .vertical
860
- container.spacing = 8
868
+ container.axis = .horizontal
869
+ container.alignment = .top
870
+ container.spacing = 10
861
871
  container.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
862
872
  container.isLayoutMarginsRelativeArrangement = true
863
- container.backgroundColor = accent.withAlphaComponent(0.1)
864
- container.layer.borderColor = accent.withAlphaComponent(0.55).cgColor
865
- container.layer.borderWidth = 1
873
+ container.backgroundColor = UIColor.secondarySystemBackground
866
874
  container.layer.cornerRadius = 10
867
875
 
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)
876
+ let emoji = UILabel()
877
+ emoji.text = stringAttr(node, "emoji") ?? "💡"
878
+ emoji.font = .systemFont(ofSize: 20)
879
+ container.addArrangedSubview(emoji)
878
880
 
881
+ let content = UIStackView()
882
+ content.axis = .vertical
883
+ content.spacing = 4
879
884
  for (index, child) in (node["children"] as? [[String: Any]] ?? []).enumerated() {
880
- container.addArrangedSubview(view(for: child, depth: depth + 1, path: path + [index]))
885
+ content.addArrangedSubview(view(for: child, depth: depth + 1, path: path + [index]))
881
886
  }
887
+ container.addArrangedSubview(content)
882
888
  return container
883
889
  }
884
890
 
@@ -916,14 +922,6 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
916
922
  }
917
923
  }
918
924
 
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
925
  private func updateNodeAttrs(_ node: [String: Any], patch: [String: Any]) {
928
926
  guard currentEditorId != 0 else { return }
929
927
  var attrs = node["attrs"] as? [String: Any] ?? [:]
@@ -1088,6 +1086,49 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
1088
1086
  return row
1089
1087
  }
1090
1088
 
1089
+ private func attachmentView(for node: [String: Any], depth: Int) -> UIView {
1090
+ let row = AttachmentRow()
1091
+ row.axis = .horizontal
1092
+ row.alignment = .center
1093
+ row.spacing = 10
1094
+ row.layoutMargins = UIEdgeInsets(top: 10, left: CGFloat(depth) * 8 + 10, bottom: 10, right: 10)
1095
+ row.isLayoutMarginsRelativeArrangement = true
1096
+ row.layer.cornerRadius = 10
1097
+ row.layer.borderWidth = 1
1098
+ row.layer.borderColor = UIColor.separator.cgColor
1099
+ let icon = UILabel()
1100
+ icon.text = "📎"
1101
+ icon.font = .systemFont(ofSize: 22)
1102
+ row.addArrangedSubview(icon)
1103
+ let details = UIStackView()
1104
+ details.axis = .vertical
1105
+ details.spacing = 2
1106
+ let name = UILabel()
1107
+ name.text = stringAttr(node, "name") ?? "Attachment"
1108
+ name.font = .systemFont(ofSize: 15, weight: .semibold)
1109
+ name.textColor = baseTextColor
1110
+ name.lineBreakMode = .byTruncatingMiddle
1111
+ let meta = UILabel()
1112
+ meta.text = stringAttr(node, "mimeType") ?? "File"
1113
+ meta.font = .systemFont(ofSize: 12)
1114
+ meta.textColor = .secondaryLabel
1115
+ details.addArrangedSubview(name)
1116
+ details.addArrangedSubview(meta)
1117
+ row.addArrangedSubview(details)
1118
+ row.accessibilityTraits = .button
1119
+ row.accessibilityLabel = "Open \(name.text ?? "attachment")"
1120
+ row.open = { [weak self] in
1121
+ self?.onAttachmentOpen?(
1122
+ self?.stringAttr(node, "attachmentId"),
1123
+ self?.stringAttr(node, "name") ?? "Attachment",
1124
+ self?.stringAttr(node, "mimeType"),
1125
+ (node["attrs"] as? [String: Any])?["size"] as? NSNumber,
1126
+ self?.stringAttr(node, "url")
1127
+ )
1128
+ }
1129
+ return row
1130
+ }
1131
+
1091
1132
  private func configureTextLeaf(_ leaf: TextLeafView, for node: [String: Any]) {
1092
1133
  let wasFirstResponder = leaf.isFirstResponder
1093
1134
  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.14",
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",