@apollohg/react-native-prose-editor 0.1.1 → 0.3.0

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.
Files changed (53) hide show
  1. package/README.md +12 -7
  2. package/android/build.gradle +7 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
  5. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
  9. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
  10. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
  12. package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
  13. package/dist/EditorTheme.js +29 -0
  14. package/dist/EditorToolbar.d.ts +129 -0
  15. package/dist/EditorToolbar.js +394 -0
  16. package/dist/NativeEditorBridge.d.ts +242 -0
  17. package/dist/NativeEditorBridge.js +647 -0
  18. package/dist/NativeRichTextEditor.d.ts +142 -0
  19. package/dist/NativeRichTextEditor.js +649 -0
  20. package/dist/YjsCollaboration.d.ts +83 -0
  21. package/dist/YjsCollaboration.js +585 -0
  22. package/dist/addons.d.ts +70 -0
  23. package/dist/addons.js +77 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +26 -0
  26. package/dist/schemas.d.ts +35 -0
  27. package/{src/schemas.ts → dist/schemas.js} +62 -27
  28. package/dist/useNativeEditor.d.ts +40 -0
  29. package/dist/useNativeEditor.js +117 -0
  30. package/ios/EditorAddons.swift +26 -3
  31. package/ios/EditorCore.xcframework/Info.plist +5 -5
  32. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  33. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  34. package/ios/EditorLayoutManager.swift +236 -0
  35. package/ios/EditorTheme.swift +51 -1
  36. package/ios/Generated_editor_core.swift +270 -2
  37. package/ios/NativeEditorExpoView.swift +612 -45
  38. package/ios/NativeEditorModule.swift +81 -0
  39. package/ios/PositionBridge.swift +22 -0
  40. package/ios/RenderBridge.swift +427 -39
  41. package/ios/RichTextEditorView.swift +1342 -18
  42. package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
  43. package/package.json +80 -64
  44. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  45. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  46. package/rust/android/x86_64/libeditor_core.so +0 -0
  47. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
  48. package/src/EditorToolbar.tsx +0 -620
  49. package/src/NativeEditorBridge.ts +0 -607
  50. package/src/NativeRichTextEditor.tsx +0 -951
  51. package/src/addons.ts +0 -158
  52. package/src/index.ts +0 -63
  53. package/src/useNativeEditor.ts +0 -173
@@ -0,0 +1,199 @@
1
+ package com.apollohg.editor
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.Paint
7
+ import android.graphics.RectF
8
+ import android.util.AttributeSet
9
+ import android.view.MotionEvent
10
+ import android.view.View
11
+ import kotlin.math.max
12
+
13
+ internal class ImageResizeOverlayView @JvmOverloads constructor(
14
+ context: Context,
15
+ attrs: AttributeSet? = null,
16
+ defStyleAttr: Int = 0
17
+ ) : View(context, attrs, defStyleAttr) {
18
+ private enum class Corner {
19
+ TOP_LEFT,
20
+ TOP_RIGHT,
21
+ BOTTOM_LEFT,
22
+ BOTTOM_RIGHT
23
+ }
24
+
25
+ private data class DragState(
26
+ val corner: Corner,
27
+ val originalRect: RectF,
28
+ val docPos: Int,
29
+ val maximumWidthPx: Float,
30
+ var previewRect: RectF
31
+ )
32
+
33
+ private var editorView: RichTextEditorView? = null
34
+ private var currentGeometry: EditorEditText.SelectedImageGeometry? = null
35
+ private var dragState: DragState? = null
36
+
37
+ private val density = resources.displayMetrics.density
38
+ private val handleRadiusPx = 10f * density
39
+ private val minimumImageSizePx = 48f * density
40
+ private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
41
+ color = Color.parseColor("#0A84FF")
42
+ style = Paint.Style.STROKE
43
+ strokeWidth = max(2f, density)
44
+ }
45
+ private val handleFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
46
+ color = Color.WHITE
47
+ style = Paint.Style.FILL
48
+ }
49
+ private val handleStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
50
+ color = Color.parseColor("#0A84FF")
51
+ style = Paint.Style.STROKE
52
+ strokeWidth = max(2f, density)
53
+ }
54
+
55
+ init {
56
+ setWillNotDraw(false)
57
+ visibility = INVISIBLE
58
+ }
59
+
60
+ fun bind(editorView: RichTextEditorView) {
61
+ this.editorView = editorView
62
+ }
63
+
64
+ fun refresh() {
65
+ currentGeometry = editorView?.selectedImageGeometry()
66
+ visibility = if (currentGeometry == null) INVISIBLE else VISIBLE
67
+ if (currentGeometry != null) {
68
+ bringToFront()
69
+ }
70
+ invalidate()
71
+ }
72
+
73
+ fun visibleRectForTesting(): RectF? =
74
+ currentGeometry?.rect?.let(::RectF)
75
+
76
+ fun simulateResizeForTesting(widthPx: Float, heightPx: Float) {
77
+ val geometry = currentGeometry ?: return
78
+ editorView?.resizeImage(geometry.docPos, widthPx, heightPx)
79
+ }
80
+
81
+ override fun onDraw(canvas: Canvas) {
82
+ super.onDraw(canvas)
83
+ val geometry = currentGeometry ?: return
84
+ canvas.drawRoundRect(geometry.rect, 8f * density, 8f * density, borderPaint)
85
+ for (corner in Corner.entries) {
86
+ val center = handleCenter(corner, geometry.rect)
87
+ canvas.drawCircle(center.x, center.y, handleRadiusPx, handleFillPaint)
88
+ canvas.drawCircle(center.x, center.y, handleRadiusPx, handleStrokePaint)
89
+ }
90
+ }
91
+
92
+ override fun onTouchEvent(event: MotionEvent): Boolean {
93
+ val geometry = currentGeometry ?: return false
94
+
95
+ when (event.actionMasked) {
96
+ MotionEvent.ACTION_DOWN -> {
97
+ val corner = cornerAt(event.x, event.y, geometry.rect) ?: return false
98
+ dragState = DragState(
99
+ corner = corner,
100
+ originalRect = RectF(geometry.rect),
101
+ docPos = geometry.docPos,
102
+ maximumWidthPx = editorView?.maximumImageWidthPx() ?: geometry.rect.width(),
103
+ previewRect = RectF(geometry.rect)
104
+ )
105
+ parent?.requestDisallowInterceptTouchEvent(true)
106
+ return true
107
+ }
108
+
109
+ MotionEvent.ACTION_MOVE -> {
110
+ val state = dragState ?: return false
111
+ val nextRect = resizedRect(
112
+ originalRect = state.originalRect,
113
+ corner = state.corner,
114
+ deltaX = event.x - handleCenter(state.corner, state.originalRect).x,
115
+ deltaY = event.y - handleCenter(state.corner, state.originalRect).y,
116
+ maximumWidthPx = state.maximumWidthPx
117
+ )
118
+ state.previewRect = RectF(nextRect)
119
+ currentGeometry = EditorEditText.SelectedImageGeometry(state.docPos, nextRect)
120
+ invalidate()
121
+ return true
122
+ }
123
+
124
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
125
+ val state = dragState ?: return false
126
+ if (event.actionMasked == MotionEvent.ACTION_UP) {
127
+ editorView?.resizeImage(
128
+ state.docPos,
129
+ state.previewRect.width(),
130
+ state.previewRect.height()
131
+ )
132
+ }
133
+ dragState = null
134
+ parent?.requestDisallowInterceptTouchEvent(false)
135
+ post { refresh() }
136
+ return true
137
+ }
138
+ }
139
+
140
+ return false
141
+ }
142
+
143
+ private fun cornerAt(x: Float, y: Float, rect: RectF): Corner? {
144
+ return Corner.entries.firstOrNull { corner ->
145
+ val center = handleCenter(corner, rect)
146
+ val dx = x - center.x
147
+ val dy = y - center.y
148
+ (dx * dx) + (dy * dy) <= handleRadiusPx * handleRadiusPx * 2
149
+ }
150
+ }
151
+
152
+ private fun handleCenter(corner: Corner, rect: RectF) = when (corner) {
153
+ Corner.TOP_LEFT -> android.graphics.PointF(rect.left, rect.top)
154
+ Corner.TOP_RIGHT -> android.graphics.PointF(rect.right, rect.top)
155
+ Corner.BOTTOM_LEFT -> android.graphics.PointF(rect.left, rect.bottom)
156
+ Corner.BOTTOM_RIGHT -> android.graphics.PointF(rect.right, rect.bottom)
157
+ }
158
+
159
+ private fun anchorPoint(corner: Corner, rect: RectF) = when (corner) {
160
+ Corner.TOP_LEFT -> android.graphics.PointF(rect.right, rect.bottom)
161
+ Corner.TOP_RIGHT -> android.graphics.PointF(rect.left, rect.bottom)
162
+ Corner.BOTTOM_LEFT -> android.graphics.PointF(rect.right, rect.top)
163
+ Corner.BOTTOM_RIGHT -> android.graphics.PointF(rect.left, rect.top)
164
+ }
165
+
166
+ private fun resizedRect(
167
+ originalRect: RectF,
168
+ corner: Corner,
169
+ deltaX: Float,
170
+ deltaY: Float,
171
+ maximumWidthPx: Float?
172
+ ): RectF {
173
+ val aspectRatio = max(originalRect.width() / max(originalRect.height(), 1f), 0.1f)
174
+ val signedDx = if (corner == Corner.TOP_RIGHT || corner == Corner.BOTTOM_RIGHT) deltaX else -deltaX
175
+ val signedDy = if (corner == Corner.BOTTOM_LEFT || corner == Corner.BOTTOM_RIGHT) deltaY else -deltaY
176
+ val widthScale = (originalRect.width() + signedDx) / max(originalRect.width(), 1f)
177
+ val heightScale = (originalRect.height() + signedDy) / max(originalRect.height(), 1f)
178
+ val scale = max(max(widthScale, heightScale), minimumImageSizePx / max(originalRect.width(), 1f))
179
+ val unclampedWidth = max(minimumImageSizePx, originalRect.width() * scale)
180
+ val unclampedHeight = max(minimumImageSizePx / aspectRatio, unclampedWidth / aspectRatio)
181
+ val (width, height) = editorView?.let { boundEditor ->
182
+ maximumWidthPx?.let { maxWidth ->
183
+ boundEditor.clampImageSize(
184
+ widthPx = unclampedWidth,
185
+ heightPx = unclampedHeight,
186
+ maximumWidthPx = maxWidth
187
+ )
188
+ } ?: boundEditor.clampImageSize(unclampedWidth, unclampedHeight)
189
+ } ?: (unclampedWidth to unclampedHeight)
190
+ val anchor = anchorPoint(corner, originalRect)
191
+
192
+ return when (corner) {
193
+ Corner.TOP_LEFT -> RectF(anchor.x - width, anchor.y - height, anchor.x, anchor.y)
194
+ Corner.TOP_RIGHT -> RectF(anchor.x, anchor.y - height, anchor.x + width, anchor.y)
195
+ Corner.BOTTOM_LEFT -> RectF(anchor.x - width, anchor.y, anchor.x, anchor.y + height)
196
+ Corner.BOTTOM_RIGHT -> RectF(anchor.x, anchor.y, anchor.x + width, anchor.y + height)
197
+ }
198
+ }
199
+ }
@@ -146,6 +146,12 @@ class NativeEditorExpoView(
146
146
  refreshMentionQuery()
147
147
  }
148
148
 
149
+ fun setRemoteSelectionsJson(remoteSelectionsJson: String?) {
150
+ richTextView.setRemoteSelections(
151
+ RemoteSelectionDecoration.fromJson(context, remoteSelectionsJson)
152
+ )
153
+ }
154
+
149
155
  fun setAutoFocus(autoFocus: Boolean) {
150
156
  if (!autoFocus || didApplyAutoFocus) {
151
157
  return
@@ -164,6 +170,10 @@ class NativeEditorExpoView(
164
170
  updateKeyboardToolbarVisibility()
165
171
  }
166
172
 
173
+ fun setAllowImageResizing(allowImageResizing: Boolean) {
174
+ richTextView.setImageResizingEnabled(allowImageResizing)
175
+ }
176
+
167
177
  fun setToolbarItemsJson(toolbarItemsJson: String?) {
168
178
  keyboardToolbarView.setItems(NativeToolbarItem.fromJson(toolbarItemsJson))
169
179
  }
@@ -306,6 +316,7 @@ class NativeEditorExpoView(
306
316
  override fun onSelectionChanged(anchor: Int, head: Int) {
307
317
  refreshToolbarStateFromEditorSelection()
308
318
  refreshMentionQuery()
319
+ richTextView.refreshRemoteSelections()
309
320
  val event = mapOf<String, Any>("anchor" to anchor, "head" to head)
310
321
  onSelectionChange(event)
311
322
  }
@@ -316,6 +327,7 @@ class NativeEditorExpoView(
316
327
  keyboardToolbarView.applyState(state)
317
328
  }
318
329
  refreshMentionQuery()
330
+ richTextView.refreshRemoteSelections()
319
331
  if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
320
332
  post {
321
333
  requestLayout()
@@ -566,8 +578,8 @@ class NativeEditorExpoView(
566
578
  val toolbarTheme = richTextView.editorEditText.theme?.toolbar
567
579
  val density = resources.displayMetrics.density
568
580
  params.gravity = Gravity.BOTTOM or Gravity.START
569
- val horizontalInsetPx = ((toolbarTheme?.horizontalInset ?: 0f) * density).toInt()
570
- val keyboardOffsetPx = ((toolbarTheme?.keyboardOffset ?: 0f) * density).toInt()
581
+ val horizontalInsetPx = ((toolbarTheme?.resolvedHorizontalInset() ?: 0f) * density).toInt()
582
+ val keyboardOffsetPx = ((toolbarTheme?.resolvedKeyboardOffset() ?: 0f) * density).toInt()
571
583
  params.leftMargin = horizontalInsetPx
572
584
  params.rightMargin = horizontalInsetPx
573
585
  params.bottomMargin = currentImeBottom + keyboardOffsetPx
@@ -611,7 +623,7 @@ class NativeEditorExpoView(
611
623
  .coerceAtLeast(0)
612
624
  val toolbarTheme = richTextView.editorEditText.theme?.toolbar
613
625
  val density = resources.displayMetrics.density
614
- val horizontalInsetPx = ((toolbarTheme?.horizontalInset ?: 0f) * density).toInt()
626
+ val horizontalInsetPx = ((toolbarTheme?.resolvedHorizontalInset() ?: 0f) * density).toInt()
615
627
  if (keyboardToolbarView.measuredHeight == 0) {
616
628
  val availableWidth = (hostWidth - horizontalInsetPx * 2).coerceAtLeast(0)
617
629
  val widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST)
@@ -630,6 +642,7 @@ class NativeEditorExpoView(
630
642
  private fun handleToolbarItemPress(item: NativeToolbarItem) {
631
643
  when (item.type) {
632
644
  ToolbarItemKind.mark -> item.mark?.let { richTextView.editorEditText.performToolbarToggleMark(it) }
645
+ ToolbarItemKind.blockquote -> richTextView.editorEditText.performToolbarToggleBlockquote()
633
646
  ToolbarItemKind.list -> item.listType?.name?.let { handleListToggle(it) }
634
647
  ToolbarItemKind.command -> when (item.command) {
635
648
  ToolbarCommand.indentList -> richTextView.editorEditText.performToolbarIndentListItem()
@@ -14,6 +14,42 @@ class NativeEditorModule : Module() {
14
14
  Function("editorDestroy") { id: Int ->
15
15
  editorDestroy(id.toULong())
16
16
  }
17
+ Function("collaborationSessionCreate") { configJson: String ->
18
+ collaborationSessionCreate(configJson).toLong()
19
+ }
20
+ Function("collaborationSessionDestroy") { id: Int ->
21
+ collaborationSessionDestroy(id.toULong())
22
+ }
23
+ Function("collaborationSessionGetDocumentJson") { id: Int ->
24
+ collaborationSessionGetDocumentJson(id.toULong())
25
+ }
26
+ Function("collaborationSessionGetEncodedState") { id: Int ->
27
+ collaborationSessionGetEncodedState(id.toULong())
28
+ }
29
+ Function("collaborationSessionGetPeersJson") { id: Int ->
30
+ collaborationSessionGetPeersJson(id.toULong())
31
+ }
32
+ Function("collaborationSessionStart") { id: Int ->
33
+ collaborationSessionStart(id.toULong())
34
+ }
35
+ Function("collaborationSessionApplyLocalDocumentJson") { id: Int, json: String ->
36
+ collaborationSessionApplyLocalDocumentJson(id.toULong(), json)
37
+ }
38
+ Function("collaborationSessionApplyEncodedState") { id: Int, encodedStateJson: String ->
39
+ collaborationSessionApplyEncodedState(id.toULong(), encodedStateJson)
40
+ }
41
+ Function("collaborationSessionReplaceEncodedState") { id: Int, encodedStateJson: String ->
42
+ collaborationSessionReplaceEncodedState(id.toULong(), encodedStateJson)
43
+ }
44
+ Function("collaborationSessionHandleMessage") { id: Int, messageJson: String ->
45
+ collaborationSessionHandleMessage(id.toULong(), messageJson)
46
+ }
47
+ Function("collaborationSessionSetLocalAwareness") { id: Int, awarenessJson: String ->
48
+ collaborationSessionSetLocalAwareness(id.toULong(), awarenessJson)
49
+ }
50
+ Function("collaborationSessionClearLocalAwareness") { id: Int ->
51
+ collaborationSessionClearLocalAwareness(id.toULong())
52
+ }
17
53
 
18
54
  Function("editorSetHtml") { id: Int, html: String ->
19
55
  editorSetHtml(id.toULong(), html)
@@ -103,6 +139,36 @@ class NativeEditorModule : Module() {
103
139
  markName
104
140
  )
105
141
  }
142
+ Function(
143
+ "editorSetMarkAtSelectionScalar"
144
+ ) { id: Int, scalarAnchor: Int, scalarHead: Int, markName: String, attrsJson: String ->
145
+ editorSetMarkAtSelectionScalar(
146
+ id.toULong(),
147
+ scalarAnchor.toUInt(),
148
+ scalarHead.toUInt(),
149
+ markName,
150
+ attrsJson
151
+ )
152
+ }
153
+ Function(
154
+ "editorUnsetMarkAtSelectionScalar"
155
+ ) { id: Int, scalarAnchor: Int, scalarHead: Int, markName: String ->
156
+ editorUnsetMarkAtSelectionScalar(
157
+ id.toULong(),
158
+ scalarAnchor.toUInt(),
159
+ scalarHead.toUInt(),
160
+ markName
161
+ )
162
+ }
163
+ Function(
164
+ "editorToggleBlockquoteAtSelectionScalar"
165
+ ) { id: Int, scalarAnchor: Int, scalarHead: Int ->
166
+ editorToggleBlockquoteAtSelectionScalar(
167
+ id.toULong(),
168
+ scalarAnchor.toUInt(),
169
+ scalarHead.toUInt()
170
+ )
171
+ }
106
172
  Function(
107
173
  "editorWrapInListAtSelectionScalar"
108
174
  ) { id: Int, scalarAnchor: Int, scalarHead: Int, listType: String ->
@@ -154,6 +220,15 @@ class NativeEditorModule : Module() {
154
220
  Function("editorToggleMark") { id: Int, markName: String ->
155
221
  editorToggleMark(id.toULong(), markName)
156
222
  }
223
+ Function("editorSetMark") { id: Int, markName: String, attrsJson: String ->
224
+ editorSetMark(id.toULong(), markName, attrsJson)
225
+ }
226
+ Function("editorUnsetMark") { id: Int, markName: String ->
227
+ editorUnsetMark(id.toULong(), markName)
228
+ }
229
+ Function("editorToggleBlockquote") { id: Int ->
230
+ editorToggleBlockquote(id.toULong())
231
+ }
157
232
 
158
233
  Function("editorSetSelection") { id: Int, anchor: Int, head: Int ->
159
234
  editorSetSelection(id.toULong(), anchor.toUInt(), head.toUInt())
@@ -205,7 +280,7 @@ class NativeEditorModule : Module() {
205
280
  view.richTextView.editorEditText.isEditable = editable
206
281
  }
207
282
  Prop("placeholder") { view: NativeEditorExpoView, placeholder: String ->
208
- view.richTextView.editorEditText.hint = placeholder
283
+ view.richTextView.editorEditText.placeholderText = placeholder
209
284
  }
210
285
  Prop("autoFocus") { view: NativeEditorExpoView, autoFocus: Boolean ->
211
286
  view.setAutoFocus(autoFocus)
@@ -219,12 +294,18 @@ class NativeEditorModule : Module() {
219
294
  Prop("heightBehavior") { view: NativeEditorExpoView, heightBehavior: String ->
220
295
  view.setHeightBehavior(heightBehavior)
221
296
  }
297
+ Prop("allowImageResizing") { view: NativeEditorExpoView, allowImageResizing: Boolean ->
298
+ view.setAllowImageResizing(allowImageResizing)
299
+ }
222
300
  Prop("themeJson") { view: NativeEditorExpoView, themeJson: String? ->
223
301
  view.setThemeJson(themeJson)
224
302
  }
225
303
  Prop("addonsJson") { view: NativeEditorExpoView, addonsJson: String? ->
226
304
  view.setAddonsJson(addonsJson)
227
305
  }
306
+ Prop("remoteSelectionsJson") { view: NativeEditorExpoView, remoteSelectionsJson: String? ->
307
+ view.setRemoteSelectionsJson(remoteSelectionsJson)
308
+ }
228
309
  Prop("toolbarItemsJson") { view: NativeEditorExpoView, toolbarItemsJson: String? ->
229
310
  view.setToolbarItemsJson(toolbarItemsJson)
230
311
  }