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

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.
@@ -42,6 +42,7 @@ import uniffi.editor_core.editorSetNodeSelection
42
42
  import uniffi.editor_core.editorSetSelection
43
43
  import uniffi.editor_core.editorSplitBlock
44
44
  import uniffi.editor_core.editorToggleTaskItemCheckedAtSelectionScalar
45
+ import uniffi.editor_core.editorUpdateNodeAttrs
45
46
 
46
47
  internal data class SelectedImageGeometry(val docPos: Int, val rect: RectF)
47
48
 
@@ -696,10 +697,6 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
696
697
  orientation = LinearLayout.HORIZONTAL
697
698
  isBaselineAligned = false
698
699
  }
699
- row.addView(
700
- markerView(node, parentListType, index),
701
- LinearLayout.LayoutParams((24 * density).toInt(), (24 * density).toInt())
702
- )
703
700
  val content = LinearLayout(context).apply {
704
701
  orientation = LinearLayout.VERTICAL
705
702
  setPadding((8 * density).toInt(), 0, 0, 0)
@@ -711,12 +708,26 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
711
708
  if (content.childCount == 0) {
712
709
  content.addView(textBlockView(node, depth + 1))
713
710
  }
711
+ val marker = if (parentListType == "toggleList" || parentListType == "toggle_list") {
712
+ val open = node.optJSONObject("attrs")?.optBoolean("open", true) ?: true
713
+ setToggleContent(content, open)
714
+ toggleMarkerView(node, content, open)
715
+ } else {
716
+ markerView(node, parentListType, index)
717
+ }
718
+ row.addView(
719
+ marker,
720
+ LinearLayout.LayoutParams((24 * density).toInt(), (24 * density).toInt())
721
+ )
714
722
  row.addView(content, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
715
723
  return row
716
724
  }
717
725
 
718
726
  private fun blockContainerView(node: JSONObject, depth: Int, path: List<Int>): View {
719
727
  val density = resources.displayMetrics.density
728
+ if (node.optString("nodeType") == "callout") {
729
+ return calloutView(node, depth, path)
730
+ }
720
731
  if (node.optString("nodeType") == "blockquote") {
721
732
  val row = LinearLayout(context).apply {
722
733
  orientation = LinearLayout.HORIZONTAL
@@ -755,6 +766,86 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
755
766
  return container
756
767
  }
757
768
 
769
+ private fun calloutView(node: JSONObject, depth: Int, path: List<Int>): View {
770
+ val density = resources.displayMetrics.density
771
+ val tone = node.optJSONObject("attrs")?.optString("tone", "info") ?: "info"
772
+ val accent = when (tone) {
773
+ "success" -> 0xFF16A34A.toInt()
774
+ "warning" -> 0xFFD97706.toInt()
775
+ "danger" -> 0xFFDC2626.toInt()
776
+ else -> 0xFF2563EB.toInt()
777
+ }
778
+ val container = LinearLayout(context).apply {
779
+ orientation = LinearLayout.VERTICAL
780
+ val padding = (12 * density).toInt()
781
+ setPadding(padding, padding, padding, padding)
782
+ background = roundedDrawable(
783
+ Color.argb(24, Color.red(accent), Color.green(accent), Color.blue(accent)),
784
+ Color.argb(140, Color.red(accent), Color.green(accent), Color.blue(accent)),
785
+ 1f,
786
+ 10f
787
+ )
788
+ }
789
+ container.addView(TextView(context).apply {
790
+ text = tone.replaceFirstChar { it.uppercase() }
791
+ setTextColor(accent)
792
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
793
+ setTypeface(typeface, Typeface.BOLD)
794
+ isClickable = this@NativeBlockEditorSurface.isEditable
795
+ setOnClickListener { cycleCalloutTone(node) }
796
+ })
797
+ val children = node.optJSONArray("children")
798
+ for (index in 0 until (children?.length() ?: 0)) {
799
+ children?.optJSONObject(index)?.let { container.addView(viewForNode(it, depth + 1, path + index)) }
800
+ }
801
+ return container
802
+ }
803
+
804
+ private fun toggleMarkerView(node: JSONObject, content: LinearLayout, initiallyOpen: Boolean): View =
805
+ TextView(context).apply {
806
+ var open = initiallyOpen
807
+ text = if (open) "⌄" else "›"
808
+ contentDescription = if (open) "Collapse toggle item" else "Expand toggle item"
809
+ gravity = android.view.Gravity.CENTER
810
+ setTextColor(currentTheme?.list?.markerColor ?: baseTextColor)
811
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
812
+ setTypeface(typeface, Typeface.BOLD)
813
+ setOnClickListener {
814
+ open = !open
815
+ text = if (open) "⌄" else "›"
816
+ contentDescription = if (open) "Collapse toggle item" else "Expand toggle item"
817
+ setToggleContent(content, open)
818
+ if (isEditable) updateNodeAttrs(node, JSONObject().put("open", open))
819
+ }
820
+ }
821
+
822
+ private fun setToggleContent(content: LinearLayout, open: Boolean) {
823
+ for (index in 1 until content.childCount) {
824
+ content.getChildAt(index).visibility = if (open) View.VISIBLE else View.GONE
825
+ }
826
+ }
827
+
828
+ private fun cycleCalloutTone(node: JSONObject) {
829
+ if (!isEditable) return
830
+ val tones = listOf("info", "success", "warning", "danger")
831
+ val current = node.optJSONObject("attrs")?.optString("tone", "info") ?: "info"
832
+ val next = tones[(tones.indexOf(current).coerceAtLeast(0) + 1) % tones.size]
833
+ updateNodeAttrs(node, JSONObject().put("tone", next))
834
+ }
835
+
836
+ private fun updateNodeAttrs(node: JSONObject, patch: JSONObject) {
837
+ if (currentEditorId == 0L) return
838
+ val attrs = JSONObject(node.optJSONObject("attrs")?.toString() ?: "{}")
839
+ patch.keys().forEach { key -> attrs.put(key, patch.get(key)) }
840
+ val updateJSON = editorUpdateNodeAttrs(
841
+ currentEditorId.toULong(),
842
+ node.optInt("docPos", 0).toUInt(),
843
+ attrs.toString()
844
+ )
845
+ applyUpdateJSON(updateJSON, currentEditorId)
846
+ onAppliedUpdate?.invoke(updateJSON)
847
+ }
848
+
758
849
  private fun tableView(node: JSONObject, path: List<Int>): View {
759
850
  val tableId = nextTableId++
760
851
  val table = LinearLayout(context).apply {
@@ -1323,6 +1323,18 @@ public func editorUnwrapFromListAtSelectionScalar(id: UInt64, scalarAnchor: UInt
1323
1323
  )
1324
1324
  })
1325
1325
  }
1326
+ /**
1327
+ * Replace a node's attributes at its document position.
1328
+ */
1329
+ public func editorUpdateNodeAttrs(id: UInt64, pos: UInt32, attrsJson: String) -> String {
1330
+ return try! FfiConverterString.lift(try! rustCall() {
1331
+ uniffi_editor_core_fn_func_editor_update_node_attrs(
1332
+ FfiConverterUInt64.lower(id),
1333
+ FfiConverterUInt32.lower(pos),
1334
+ FfiConverterString.lower(attrsJson),$0
1335
+ )
1336
+ })
1337
+ }
1326
1338
  /**
1327
1339
  * Wrap the current selection in a list. Returns an update JSON string.
1328
1340
  */
@@ -1582,6 +1594,9 @@ private let initializationResult: InitializationResult = {
1582
1594
  if (uniffi_editor_core_checksum_func_editor_unwrap_from_list_at_selection_scalar() != 57899) {
1583
1595
  return InitializationResult.apiChecksumMismatch
1584
1596
  }
1597
+ if (uniffi_editor_core_checksum_func_editor_update_node_attrs() != 14684) {
1598
+ return InitializationResult.apiChecksumMismatch
1599
+ }
1585
1600
  if (uniffi_editor_core_checksum_func_editor_wrap_in_list() != 32846) {
1586
1601
  return InitializationResult.apiChecksumMismatch
1587
1602
  }
@@ -785,8 +785,6 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
785
785
  row.alignment = .top
786
786
  row.spacing = 8
787
787
 
788
- row.addArrangedSubview(markerView(for: node, parentListType: parentListType, index: index))
789
-
790
788
  let content = UIStackView()
791
789
  content.axis = .vertical
792
790
  content.spacing = 4
@@ -796,11 +794,20 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
796
794
  if content.arrangedSubviews.isEmpty {
797
795
  content.addArrangedSubview(textBlockView(for: node, depth: depth + 1))
798
796
  }
797
+ if parentListType == "toggleList" || parentListType == "toggle_list" {
798
+ row.addArrangedSubview(toggleMarkerView(for: node, content: content))
799
+ setToggleContent(content, open: boolAttr(node, "open") ?? true)
800
+ } else {
801
+ row.addArrangedSubview(markerView(for: node, parentListType: parentListType, index: index))
802
+ }
799
803
  row.addArrangedSubview(content)
800
804
  return row
801
805
  }
802
806
 
803
807
  private func blockContainerView(for node: [String: Any], depth: Int, path: [Int]) -> UIView {
808
+ if (node["nodeType"] as? String) == "callout" {
809
+ return calloutView(for: node, depth: depth, path: path)
810
+ }
804
811
  let container = UIStackView()
805
812
  container.axis = .vertical
806
813
  container.spacing = 4
@@ -834,6 +841,96 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
834
841
  return container
835
842
  }
836
843
 
844
+ private func calloutView(for node: [String: Any], depth: Int, path: [Int]) -> UIView {
845
+ let tone = stringAttr(node, "tone") ?? "info"
846
+ let accent: UIColor = switch tone {
847
+ case "success": .systemGreen
848
+ case "warning": .systemOrange
849
+ case "danger": .systemRed
850
+ default: .systemBlue
851
+ }
852
+ let container = UIStackView()
853
+ container.axis = .vertical
854
+ container.spacing = 8
855
+ container.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
856
+ container.isLayoutMarginsRelativeArrangement = true
857
+ container.backgroundColor = accent.withAlphaComponent(0.1)
858
+ container.layer.borderColor = accent.withAlphaComponent(0.55).cgColor
859
+ container.layer.borderWidth = 1
860
+ container.layer.cornerRadius = 10
861
+
862
+ let toneButton = UIButton(type: .system)
863
+ toneButton.setTitle(tone.capitalized, for: .normal)
864
+ toneButton.setTitleColor(accent, for: .normal)
865
+ toneButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .semibold)
866
+ toneButton.contentHorizontalAlignment = .leading
867
+ toneButton.isEnabled = isEditable
868
+ toneButton.addAction(UIAction { [weak self] _ in
869
+ self?.cycleCalloutTone(node)
870
+ }, for: .touchUpInside)
871
+ container.addArrangedSubview(toneButton)
872
+
873
+ for (index, child) in (node["children"] as? [[String: Any]] ?? []).enumerated() {
874
+ container.addArrangedSubview(view(for: child, depth: depth + 1, path: path + [index]))
875
+ }
876
+ return container
877
+ }
878
+
879
+ private func toggleMarkerView(for node: [String: Any], content: UIStackView) -> UIView {
880
+ let open = boolAttr(node, "open") ?? true
881
+ let button = UIButton(type: .system)
882
+ button.setTitle(open ? "⌄" : "›", for: .normal)
883
+ button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
884
+ button.accessibilityLabel = open ? "Collapse toggle item" : "Expand toggle item"
885
+ button.addAction(UIAction { [weak self, weak button, weak content] _ in
886
+ guard let self, let button, let content else { return }
887
+ let nextOpen = button.title(for: .normal) != "⌄"
888
+ button.setTitle(nextOpen ? "⌄" : "›", for: .normal)
889
+ button.accessibilityLabel = nextOpen ? "Collapse toggle item" : "Expand toggle item"
890
+ self.setToggleContent(content, open: nextOpen)
891
+ if self.isEditable {
892
+ self.updateNodeAttrs(node, patch: ["open": nextOpen])
893
+ }
894
+ }, for: .touchUpInside)
895
+ let wrapper = UIView()
896
+ wrapper.widthAnchor.constraint(equalToConstant: 24).isActive = true
897
+ wrapper.heightAnchor.constraint(equalToConstant: 24).isActive = true
898
+ button.translatesAutoresizingMaskIntoConstraints = false
899
+ wrapper.addSubview(button)
900
+ NSLayoutConstraint.activate([
901
+ button.centerXAnchor.constraint(equalTo: wrapper.centerXAnchor),
902
+ button.centerYAnchor.constraint(equalTo: wrapper.centerYAnchor),
903
+ ])
904
+ return wrapper
905
+ }
906
+
907
+ private func setToggleContent(_ content: UIStackView, open: Bool) {
908
+ for (index, child) in content.arrangedSubviews.enumerated() where index > 0 {
909
+ child.isHidden = !open
910
+ }
911
+ }
912
+
913
+ private func cycleCalloutTone(_ node: [String: Any]) {
914
+ guard isEditable else { return }
915
+ let tones = ["info", "success", "warning", "danger"]
916
+ let current = stringAttr(node, "tone") ?? "info"
917
+ let next = tones[((tones.firstIndex(of: current) ?? 0) + 1) % tones.count]
918
+ updateNodeAttrs(node, patch: ["tone": next])
919
+ }
920
+
921
+ private func updateNodeAttrs(_ node: [String: Any], patch: [String: Any]) {
922
+ guard currentEditorId != 0 else { return }
923
+ var attrs = node["attrs"] as? [String: Any] ?? [:]
924
+ attrs.merge(patch) { _, next in next }
925
+ guard let data = try? JSONSerialization.data(withJSONObject: attrs),
926
+ let json = String(data: data, encoding: .utf8)
927
+ else { return }
928
+ let pos = (node["docPos"] as? NSNumber)?.uint32Value ?? 0
929
+ let updateJSON = editorUpdateNodeAttrs(id: currentEditorId, pos: pos, attrsJson: json)
930
+ applyUpdateJSON(updateJSON, editorId: currentEditorId)
931
+ onAppliedUpdate?(updateJSON)
932
+ }
933
+
837
934
  func blockquoteStripeRectsForTesting() -> [CGRect] {
838
935
  layoutIfNeeded()
839
936
  return descendants(of: self)
@@ -1549,6 +1646,10 @@ final class NativeBlockEditorSurface: UIScrollView, UITextViewDelegate, UIGestur
1549
1646
  return attrs[key] as? Bool
1550
1647
  }
1551
1648
 
1649
+ private func stringAttr(_ node: [String: Any], _ key: String) -> String? {
1650
+ (node["attrs"] as? [String: Any])?[key] as? String
1651
+ }
1652
+
1552
1653
  private func isCodeBlock(_ node: [String: Any]) -> Bool {
1553
1654
  let nodeType = node["nodeType"] as? String
1554
1655
  return nodeType == "codeBlock" || nodeType == "code_block"
@@ -617,6 +617,11 @@ RustBuffer uniffi_editor_core_fn_func_editor_unwrap_from_list(uint64_t id, RustC
617
617
  RustBuffer uniffi_editor_core_fn_func_editor_unwrap_from_list_at_selection_scalar(uint64_t id, uint32_t scalar_anchor, uint32_t scalar_head, RustCallStatus *_Nonnull out_status
618
618
  );
619
619
  #endif
620
+ #ifndef UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_FN_FUNC_EDITOR_UPDATE_NODE_ATTRS
621
+ #define UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_FN_FUNC_EDITOR_UPDATE_NODE_ATTRS
622
+ RustBuffer uniffi_editor_core_fn_func_editor_update_node_attrs(uint64_t id, uint32_t pos, RustBuffer attrs_json, RustCallStatus *_Nonnull out_status
623
+ );
624
+ #endif
620
625
  #ifndef UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_FN_FUNC_EDITOR_WRAP_IN_LIST
621
626
  #define UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_FN_FUNC_EDITOR_WRAP_IN_LIST
622
627
  RustBuffer uniffi_editor_core_fn_func_editor_wrap_in_list(uint64_t id, RustBuffer list_type, RustCallStatus *_Nonnull out_status
@@ -1343,6 +1348,12 @@ uint16_t uniffi_editor_core_checksum_func_editor_unwrap_from_list(void
1343
1348
  #define UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_CHECKSUM_FUNC_EDITOR_UNWRAP_FROM_LIST_AT_SELECTION_SCALAR
1344
1349
  uint16_t uniffi_editor_core_checksum_func_editor_unwrap_from_list_at_selection_scalar(void
1345
1350
 
1351
+ );
1352
+ #endif
1353
+ #ifndef UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_CHECKSUM_FUNC_EDITOR_UPDATE_NODE_ATTRS
1354
+ #define UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_CHECKSUM_FUNC_EDITOR_UPDATE_NODE_ATTRS
1355
+ uint16_t uniffi_editor_core_checksum_func_editor_update_node_attrs(void
1356
+
1346
1357
  );
1347
1358
  #endif
1348
1359
  #ifndef UNIFFI_FFIDEF_UNIFFI_EDITOR_CORE_CHECKSUM_FUNC_EDITOR_WRAP_IN_LIST
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openeditor/react-native-prose-editor",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
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",
@@ -911,6 +911,8 @@ internal interface IntegrityCheckingUniffiLib : Library {
911
911
 
912
912
  fun uniffi_editor_core_checksum_func_editor_unwrap_from_list_at_selection_scalar(): Short
913
913
 
914
+ fun uniffi_editor_core_checksum_func_editor_update_node_attrs(): Short
915
+
914
916
  fun uniffi_editor_core_checksum_func_editor_wrap_in_list(): Short
915
917
 
916
918
  fun uniffi_editor_core_checksum_func_editor_wrap_in_list_at_selection_scalar(): Short
@@ -1398,6 +1400,13 @@ internal interface UniffiLib : Library {
1398
1400
  uniffi_out_err: UniffiRustCallStatus,
1399
1401
  ): RustBuffer.ByValue
1400
1402
 
1403
+ fun uniffi_editor_core_fn_func_editor_update_node_attrs(
1404
+ `id`: Long,
1405
+ `pos`: Int,
1406
+ `attrsJson`: RustBuffer.ByValue,
1407
+ uniffi_out_err: UniffiRustCallStatus,
1408
+ ): RustBuffer.ByValue
1409
+
1401
1410
  fun uniffi_editor_core_fn_func_editor_wrap_in_list(
1402
1411
  `id`: Long,
1403
1412
  `listType`: RustBuffer.ByValue,
@@ -1860,6 +1869,9 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) {
1860
1869
  if (lib.uniffi_editor_core_checksum_func_editor_unwrap_from_list_at_selection_scalar() != 57899.toShort()) {
1861
1870
  throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
1862
1871
  }
1872
+ if (lib.uniffi_editor_core_checksum_func_editor_update_node_attrs() != 14684.toShort()) {
1873
+ throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
1874
+ }
1863
1875
  if (lib.uniffi_editor_core_checksum_func_editor_wrap_in_list() != 32846.toShort()) {
1864
1876
  throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project")
1865
1877
  }
@@ -3212,6 +3224,25 @@ fun `editorUnwrapFromListAtSelectionScalar`(
3212
3224
  },
3213
3225
  )
3214
3226
 
3227
+ /**
3228
+ * Replace a node's attributes at its document position.
3229
+ */
3230
+ fun `editorUpdateNodeAttrs`(
3231
+ `id`: kotlin.ULong,
3232
+ `pos`: kotlin.UInt,
3233
+ `attrsJson`: kotlin.String,
3234
+ ): kotlin.String =
3235
+ FfiConverterString.lift(
3236
+ uniffiRustCall { _status ->
3237
+ UniffiLib.INSTANCE.uniffi_editor_core_fn_func_editor_update_node_attrs(
3238
+ FfiConverterULong.lower(`id`),
3239
+ FfiConverterUInt.lower(`pos`),
3240
+ FfiConverterString.lower(`attrsJson`),
3241
+ _status,
3242
+ )
3243
+ },
3244
+ )
3245
+
3215
3246
  /**
3216
3247
  * Wrap the current selection in a list. Returns an update JSON string.
3217
3248
  */