@openeditor/react-native-prose-editor 0.0.2
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.
- package/LICENSE +160 -0
- package/README.md +180 -0
- package/android/build.gradle +69 -0
- package/android/consumer-rules.pro +18 -0
- package/android/src/androidTest/AndroidManifest.xml +8 -0
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
- package/app.plugin.js +62 -0
- package/dist/EditorTheme.d.ts +113 -0
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +191 -0
- package/dist/EditorToolbar.js +1095 -0
- package/dist/NativeEditorBridge.d.ts +325 -0
- package/dist/NativeEditorBridge.js +933 -0
- package/dist/NativeProseViewer.d.ts +53 -0
- package/dist/NativeProseViewer.js +480 -0
- package/dist/NativeRichTextEditor.d.ts +183 -0
- package/dist/NativeRichTextEditor.js +2574 -0
- package/dist/YjsCollaboration.d.ts +90 -0
- package/dist/YjsCollaboration.js +743 -0
- package/dist/addons.d.ts +100 -0
- package/dist/addons.js +82 -0
- package/dist/heightCache.d.ts +6 -0
- package/dist/heightCache.js +45 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +30 -0
- package/dist/schemas.d.ts +37 -0
- package/dist/schemas.js +263 -0
- package/dist/useNativeEditor.d.ts +42 -0
- package/dist/useNativeEditor.js +124 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +327 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +774 -0
- package/ios/EditorTheme.swift +515 -0
- package/ios/Generated_editor_core.swift +1513 -0
- package/ios/NativeEditorExpoView.swift +3593 -0
- package/ios/NativeEditorModule.swift +624 -0
- package/ios/NativeProseViewerExpoView.swift +276 -0
- package/ios/PositionBridge.swift +558 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +1708 -0
- package/ios/RichTextEditorView.swift +6150 -0
- package/ios/Tests/NativePerformanceTests.swift +687 -0
- package/ios/Tests/PositionBridgeTests.swift +706 -0
- package/ios/Tests/RenderBridgeTests.swift +2236 -0
- package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +74 -0
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
package com.openeditor.editor
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
|
|
7
|
+
data class EditorTextStyle(
|
|
8
|
+
val fontFamily: String? = null,
|
|
9
|
+
val fontSize: Float? = null,
|
|
10
|
+
val fontWeight: String? = null,
|
|
11
|
+
val fontStyle: String? = null,
|
|
12
|
+
val color: Int? = null,
|
|
13
|
+
val lineHeight: Float? = null,
|
|
14
|
+
val spacingAfter: Float? = null
|
|
15
|
+
) {
|
|
16
|
+
companion object {
|
|
17
|
+
fun fromJson(json: JSONObject?): EditorTextStyle? {
|
|
18
|
+
json ?: return null
|
|
19
|
+
return EditorTextStyle(
|
|
20
|
+
fontFamily = json.optNullableString("fontFamily"),
|
|
21
|
+
fontSize = json.optNullableFloat("fontSize"),
|
|
22
|
+
fontWeight = json.optNullableString("fontWeight"),
|
|
23
|
+
fontStyle = json.optNullableString("fontStyle"),
|
|
24
|
+
color = parseColor(json.optNullableString("color")),
|
|
25
|
+
lineHeight = json.optNullableFloat("lineHeight"),
|
|
26
|
+
spacingAfter = json.optNullableFloat("spacingAfter")
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun mergedWith(other: EditorTextStyle?): EditorTextStyle {
|
|
32
|
+
other ?: return this
|
|
33
|
+
return copy(
|
|
34
|
+
fontFamily = other.fontFamily ?: fontFamily,
|
|
35
|
+
fontSize = other.fontSize ?: fontSize,
|
|
36
|
+
fontWeight = other.fontWeight ?: fontWeight,
|
|
37
|
+
fontStyle = other.fontStyle ?: fontStyle,
|
|
38
|
+
color = other.color ?: color,
|
|
39
|
+
lineHeight = other.lineHeight ?: lineHeight,
|
|
40
|
+
spacingAfter = other.spacingAfter ?: spacingAfter
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fun typefaceStyle(): Int {
|
|
45
|
+
val bold = fontWeight == "bold" || fontWeight?.toIntOrNull()?.let { it >= 600 } == true
|
|
46
|
+
val italic = fontStyle == "italic"
|
|
47
|
+
return when {
|
|
48
|
+
bold && italic -> Typeface.BOLD_ITALIC
|
|
49
|
+
bold -> Typeface.BOLD
|
|
50
|
+
italic -> Typeface.ITALIC
|
|
51
|
+
else -> Typeface.NORMAL
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
data class EditorListTheme(
|
|
57
|
+
val indent: Float? = null,
|
|
58
|
+
val baseIndentMultiplier: Float? = null,
|
|
59
|
+
val itemSpacing: Float? = null,
|
|
60
|
+
val markerColor: Int? = null,
|
|
61
|
+
val markerScale: Float? = null
|
|
62
|
+
) {
|
|
63
|
+
companion object {
|
|
64
|
+
fun fromJson(json: JSONObject?): EditorListTheme? {
|
|
65
|
+
json ?: return null
|
|
66
|
+
return EditorListTheme(
|
|
67
|
+
indent = json.optNullableFloat("indent"),
|
|
68
|
+
baseIndentMultiplier = json.optNullableFloat("baseIndentMultiplier"),
|
|
69
|
+
itemSpacing = json.optNullableFloat("itemSpacing"),
|
|
70
|
+
markerColor = parseColor(json.optNullableString("markerColor")),
|
|
71
|
+
markerScale = json.optNullableFloat("markerScale")
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
data class EditorHorizontalRuleTheme(
|
|
78
|
+
val color: Int? = null,
|
|
79
|
+
val thickness: Float? = null,
|
|
80
|
+
val verticalMargin: Float? = null
|
|
81
|
+
) {
|
|
82
|
+
companion object {
|
|
83
|
+
fun fromJson(json: JSONObject?): EditorHorizontalRuleTheme? {
|
|
84
|
+
json ?: return null
|
|
85
|
+
return EditorHorizontalRuleTheme(
|
|
86
|
+
color = parseColor(json.optNullableString("color")),
|
|
87
|
+
thickness = json.optNullableFloat("thickness"),
|
|
88
|
+
verticalMargin = json.optNullableFloat("verticalMargin")
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
data class EditorBlockquoteTheme(
|
|
95
|
+
val text: EditorTextStyle? = null,
|
|
96
|
+
val indent: Float? = null,
|
|
97
|
+
val borderColor: Int? = null,
|
|
98
|
+
val borderWidth: Float? = null,
|
|
99
|
+
val markerGap: Float? = null
|
|
100
|
+
) {
|
|
101
|
+
companion object {
|
|
102
|
+
fun fromJson(json: JSONObject?): EditorBlockquoteTheme? {
|
|
103
|
+
json ?: return null
|
|
104
|
+
return EditorBlockquoteTheme(
|
|
105
|
+
text = EditorTextStyle.fromJson(json.optJSONObject("text")),
|
|
106
|
+
indent = json.optNullableFloat("indent"),
|
|
107
|
+
borderColor = parseColor(json.optNullableString("borderColor")),
|
|
108
|
+
borderWidth = json.optNullableFloat("borderWidth"),
|
|
109
|
+
markerGap = json.optNullableFloat("markerGap")
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
data class EditorCodeBlockTheme(
|
|
116
|
+
val text: EditorTextStyle? = null,
|
|
117
|
+
val backgroundColor: Int? = null,
|
|
118
|
+
val borderRadius: Float? = null,
|
|
119
|
+
val paddingHorizontal: Float? = null,
|
|
120
|
+
val paddingVertical: Float? = null
|
|
121
|
+
) {
|
|
122
|
+
companion object {
|
|
123
|
+
fun fromJson(json: JSONObject?): EditorCodeBlockTheme? {
|
|
124
|
+
json ?: return null
|
|
125
|
+
return EditorCodeBlockTheme(
|
|
126
|
+
text = EditorTextStyle.fromJson(json.optJSONObject("text")),
|
|
127
|
+
backgroundColor = parseColor(json.optNullableString("backgroundColor")),
|
|
128
|
+
borderRadius = json.optNullableFloat("borderRadius"),
|
|
129
|
+
paddingHorizontal = json.optNullableFloat("paddingHorizontal"),
|
|
130
|
+
paddingVertical = json.optNullableFloat("paddingVertical")
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
data class EditorLinkTheme(
|
|
137
|
+
val fontFamily: String? = null,
|
|
138
|
+
val fontSize: Float? = null,
|
|
139
|
+
val fontWeight: String? = null,
|
|
140
|
+
val fontStyle: String? = null,
|
|
141
|
+
val color: Int? = null,
|
|
142
|
+
val backgroundColor: Int? = null,
|
|
143
|
+
val underline: Boolean? = null
|
|
144
|
+
) {
|
|
145
|
+
companion object {
|
|
146
|
+
fun fromJson(json: JSONObject?): EditorLinkTheme? {
|
|
147
|
+
json ?: return null
|
|
148
|
+
return EditorLinkTheme(
|
|
149
|
+
fontFamily = json.optNullableString("fontFamily"),
|
|
150
|
+
fontSize = json.optNullableFloat("fontSize"),
|
|
151
|
+
fontWeight = json.optNullableString("fontWeight"),
|
|
152
|
+
fontStyle = json.optNullableString("fontStyle"),
|
|
153
|
+
color = parseColor(json.optNullableString("color")),
|
|
154
|
+
backgroundColor = parseColor(json.optNullableString("backgroundColor")),
|
|
155
|
+
underline = if (json.has("underline")) json.optBoolean("underline") else null
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fun asTextStyle(): EditorTextStyle =
|
|
161
|
+
EditorTextStyle(
|
|
162
|
+
fontFamily = fontFamily,
|
|
163
|
+
fontSize = fontSize,
|
|
164
|
+
fontWeight = fontWeight,
|
|
165
|
+
fontStyle = fontStyle,
|
|
166
|
+
color = color
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
data class EditorMentionTheme(
|
|
171
|
+
val textColor: Int? = null,
|
|
172
|
+
val backgroundColor: Int? = null,
|
|
173
|
+
val borderColor: Int? = null,
|
|
174
|
+
val borderWidth: Float? = null,
|
|
175
|
+
val borderRadius: Float? = null,
|
|
176
|
+
val fontWeight: String? = null,
|
|
177
|
+
val popoverBackgroundColor: Int? = null,
|
|
178
|
+
val popoverBorderColor: Int? = null,
|
|
179
|
+
val popoverBorderWidth: Float? = null,
|
|
180
|
+
val popoverBorderRadius: Float? = null,
|
|
181
|
+
val popoverShadowColor: Int? = null,
|
|
182
|
+
val optionTextColor: Int? = null,
|
|
183
|
+
val optionSecondaryTextColor: Int? = null,
|
|
184
|
+
val optionHighlightedBackgroundColor: Int? = null,
|
|
185
|
+
val optionHighlightedTextColor: Int? = null
|
|
186
|
+
) {
|
|
187
|
+
fun mergedWith(other: EditorMentionTheme?): EditorMentionTheme {
|
|
188
|
+
other ?: return this
|
|
189
|
+
return copy(
|
|
190
|
+
textColor = other.textColor ?: textColor,
|
|
191
|
+
backgroundColor = other.backgroundColor ?: backgroundColor,
|
|
192
|
+
borderColor = other.borderColor ?: borderColor,
|
|
193
|
+
borderWidth = other.borderWidth ?: borderWidth,
|
|
194
|
+
borderRadius = other.borderRadius ?: borderRadius,
|
|
195
|
+
fontWeight = other.fontWeight ?: fontWeight,
|
|
196
|
+
popoverBackgroundColor = other.popoverBackgroundColor ?: popoverBackgroundColor,
|
|
197
|
+
popoverBorderColor = other.popoverBorderColor ?: popoverBorderColor,
|
|
198
|
+
popoverBorderWidth = other.popoverBorderWidth ?: popoverBorderWidth,
|
|
199
|
+
popoverBorderRadius = other.popoverBorderRadius ?: popoverBorderRadius,
|
|
200
|
+
popoverShadowColor = other.popoverShadowColor ?: popoverShadowColor,
|
|
201
|
+
optionTextColor = other.optionTextColor ?: optionTextColor,
|
|
202
|
+
optionSecondaryTextColor = other.optionSecondaryTextColor ?: optionSecondaryTextColor,
|
|
203
|
+
optionHighlightedBackgroundColor =
|
|
204
|
+
other.optionHighlightedBackgroundColor ?: optionHighlightedBackgroundColor,
|
|
205
|
+
optionHighlightedTextColor =
|
|
206
|
+
other.optionHighlightedTextColor ?: optionHighlightedTextColor
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
companion object {
|
|
211
|
+
fun fromJson(json: JSONObject?): EditorMentionTheme? {
|
|
212
|
+
json ?: return null
|
|
213
|
+
return EditorMentionTheme(
|
|
214
|
+
textColor = parseColor(json.optNullableString("textColor")),
|
|
215
|
+
backgroundColor = parseColor(json.optNullableString("backgroundColor")),
|
|
216
|
+
borderColor = parseColor(json.optNullableString("borderColor")),
|
|
217
|
+
borderWidth = json.optNullableFloat("borderWidth"),
|
|
218
|
+
borderRadius = json.optNullableFloat("borderRadius"),
|
|
219
|
+
fontWeight = json.optNullableString("fontWeight"),
|
|
220
|
+
popoverBackgroundColor = parseColor(json.optNullableString("popoverBackgroundColor")),
|
|
221
|
+
popoverBorderColor = parseColor(json.optNullableString("popoverBorderColor")),
|
|
222
|
+
popoverBorderWidth = json.optNullableFloat("popoverBorderWidth"),
|
|
223
|
+
popoverBorderRadius = json.optNullableFloat("popoverBorderRadius"),
|
|
224
|
+
popoverShadowColor = parseColor(json.optNullableString("popoverShadowColor")),
|
|
225
|
+
optionTextColor = parseColor(json.optNullableString("optionTextColor")),
|
|
226
|
+
optionSecondaryTextColor = parseColor(json.optNullableString("optionSecondaryTextColor")),
|
|
227
|
+
optionHighlightedBackgroundColor = parseColor(json.optNullableString("optionHighlightedBackgroundColor")),
|
|
228
|
+
optionHighlightedTextColor = parseColor(json.optNullableString("optionHighlightedTextColor"))
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
enum class EditorToolbarAppearance {
|
|
235
|
+
CUSTOM,
|
|
236
|
+
NATIVE;
|
|
237
|
+
|
|
238
|
+
companion object {
|
|
239
|
+
fun fromRaw(raw: String?): EditorToolbarAppearance? =
|
|
240
|
+
when (raw?.trim()?.lowercase()) {
|
|
241
|
+
"custom" -> CUSTOM
|
|
242
|
+
"native" -> NATIVE
|
|
243
|
+
else -> null
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
data class EditorToolbarTheme(
|
|
249
|
+
val appearance: EditorToolbarAppearance? = null,
|
|
250
|
+
val height: Float? = null,
|
|
251
|
+
val backgroundColor: Int? = null,
|
|
252
|
+
val borderColor: Int? = null,
|
|
253
|
+
val borderWidth: Float? = null,
|
|
254
|
+
val borderRadius: Float? = null,
|
|
255
|
+
val marginTop: Float? = null,
|
|
256
|
+
val showTopBorder: Boolean? = null,
|
|
257
|
+
val keyboardOffset: Float? = null,
|
|
258
|
+
val horizontalInset: Float? = null,
|
|
259
|
+
val separatorColor: Int? = null,
|
|
260
|
+
val buttonColor: Int? = null,
|
|
261
|
+
val buttonActiveColor: Int? = null,
|
|
262
|
+
val buttonDisabledColor: Int? = null,
|
|
263
|
+
val buttonActiveBackgroundColor: Int? = null,
|
|
264
|
+
val buttonBorderRadius: Float? = null
|
|
265
|
+
) {
|
|
266
|
+
fun resolvedKeyboardOffset(): Float = keyboardOffset ?: if (appearance == EditorToolbarAppearance.NATIVE) 8f else 0f
|
|
267
|
+
|
|
268
|
+
fun resolvedHorizontalInset(): Float = horizontalInset ?: if (appearance == EditorToolbarAppearance.NATIVE) 0f else 0f
|
|
269
|
+
|
|
270
|
+
fun resolvedBorderRadius(): Float = if (appearance == EditorToolbarAppearance.NATIVE) 32f else (borderRadius ?: 0f)
|
|
271
|
+
|
|
272
|
+
fun resolvedBorderWidth(): Float = borderWidth ?: if (appearance == EditorToolbarAppearance.NATIVE) 0f else 1f
|
|
273
|
+
|
|
274
|
+
fun resolvedButtonBorderRadius(): Float = if (appearance == EditorToolbarAppearance.NATIVE) 20f else (buttonBorderRadius ?: 6f)
|
|
275
|
+
|
|
276
|
+
companion object {
|
|
277
|
+
fun fromJson(json: JSONObject?): EditorToolbarTheme? {
|
|
278
|
+
json ?: return null
|
|
279
|
+
return EditorToolbarTheme(
|
|
280
|
+
appearance = EditorToolbarAppearance.fromRaw(json.optNullableString("appearance")),
|
|
281
|
+
height = json.optNullableFloat("height"),
|
|
282
|
+
backgroundColor = parseColor(json.optNullableString("backgroundColor")),
|
|
283
|
+
borderColor = parseColor(json.optNullableString("borderColor")),
|
|
284
|
+
borderWidth = json.optNullableFloat("borderWidth"),
|
|
285
|
+
borderRadius = json.optNullableFloat("borderRadius"),
|
|
286
|
+
marginTop = json.optNullableFloat("marginTop"),
|
|
287
|
+
showTopBorder = if (json.has("showTopBorder")) json.optBoolean("showTopBorder") else null,
|
|
288
|
+
keyboardOffset = json.optNullableFloat("keyboardOffset"),
|
|
289
|
+
horizontalInset = json.optNullableFloat("horizontalInset"),
|
|
290
|
+
separatorColor = parseColor(json.optNullableString("separatorColor")),
|
|
291
|
+
buttonColor = parseColor(json.optNullableString("buttonColor")),
|
|
292
|
+
buttonActiveColor = parseColor(json.optNullableString("buttonActiveColor")),
|
|
293
|
+
buttonDisabledColor = parseColor(json.optNullableString("buttonDisabledColor")),
|
|
294
|
+
buttonActiveBackgroundColor = parseColor(json.optNullableString("buttonActiveBackgroundColor")),
|
|
295
|
+
buttonBorderRadius = json.optNullableFloat("buttonBorderRadius")
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
data class EditorContentInsets(
|
|
302
|
+
val top: Float? = null,
|
|
303
|
+
val right: Float? = null,
|
|
304
|
+
val bottom: Float? = null,
|
|
305
|
+
val left: Float? = null
|
|
306
|
+
) {
|
|
307
|
+
companion object {
|
|
308
|
+
fun fromJson(json: JSONObject?): EditorContentInsets? {
|
|
309
|
+
json ?: return null
|
|
310
|
+
return EditorContentInsets(
|
|
311
|
+
top = json.optNullableFloat("top"),
|
|
312
|
+
right = json.optNullableFloat("right"),
|
|
313
|
+
bottom = json.optNullableFloat("bottom"),
|
|
314
|
+
left = json.optNullableFloat("left")
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
data class EditorTheme(
|
|
321
|
+
val text: EditorTextStyle? = null,
|
|
322
|
+
val paragraph: EditorTextStyle? = null,
|
|
323
|
+
val blockquote: EditorBlockquoteTheme? = null,
|
|
324
|
+
val codeBlock: EditorCodeBlockTheme? = null,
|
|
325
|
+
val headings: Map<String, EditorTextStyle> = emptyMap(),
|
|
326
|
+
val list: EditorListTheme? = null,
|
|
327
|
+
val horizontalRule: EditorHorizontalRuleTheme? = null,
|
|
328
|
+
val mentions: EditorMentionTheme? = null,
|
|
329
|
+
val links: EditorLinkTheme? = null,
|
|
330
|
+
val toolbar: EditorToolbarTheme? = null,
|
|
331
|
+
val placeholderColor: Int? = null,
|
|
332
|
+
val backgroundColor: Int? = null,
|
|
333
|
+
val borderRadius: Float? = null,
|
|
334
|
+
val contentInsets: EditorContentInsets? = null
|
|
335
|
+
) {
|
|
336
|
+
companion object {
|
|
337
|
+
fun fromJson(json: String?): EditorTheme? {
|
|
338
|
+
if (json.isNullOrBlank()) return null
|
|
339
|
+
val root = try {
|
|
340
|
+
JSONObject(json)
|
|
341
|
+
} catch (_: Exception) {
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
val headings = mutableMapOf<String, EditorTextStyle>()
|
|
346
|
+
for (level in listOf("h1", "h2", "h3", "h4", "h5", "h6")) {
|
|
347
|
+
val style = EditorTextStyle.fromJson(root.optJSONObject("headings")?.optJSONObject(level))
|
|
348
|
+
if (style != null) {
|
|
349
|
+
headings[level] = style
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return EditorTheme(
|
|
354
|
+
text = EditorTextStyle.fromJson(root.optJSONObject("text")),
|
|
355
|
+
paragraph = EditorTextStyle.fromJson(root.optJSONObject("paragraph")),
|
|
356
|
+
blockquote = EditorBlockquoteTheme.fromJson(root.optJSONObject("blockquote")),
|
|
357
|
+
codeBlock = EditorCodeBlockTheme.fromJson(root.optJSONObject("codeBlock")),
|
|
358
|
+
headings = headings,
|
|
359
|
+
list = EditorListTheme.fromJson(root.optJSONObject("list")),
|
|
360
|
+
horizontalRule = EditorHorizontalRuleTheme.fromJson(root.optJSONObject("horizontalRule")),
|
|
361
|
+
mentions = EditorMentionTheme.fromJson(root.optJSONObject("mentions")),
|
|
362
|
+
links = EditorLinkTheme.fromJson(root.optJSONObject("links")),
|
|
363
|
+
toolbar = EditorToolbarTheme.fromJson(root.optJSONObject("toolbar")),
|
|
364
|
+
placeholderColor = parseColor(root.optNullableString("placeholderColor")),
|
|
365
|
+
backgroundColor = parseColor(root.optNullableString("backgroundColor")),
|
|
366
|
+
borderRadius = root.optNullableFloat("borderRadius"),
|
|
367
|
+
contentInsets = EditorContentInsets.fromJson(root.optJSONObject("contentInsets"))
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
fun effectiveTextStyle(nodeType: String, inBlockquote: Boolean = false): EditorTextStyle {
|
|
373
|
+
var style = text ?: EditorTextStyle()
|
|
374
|
+
style = style.mergedWith(if (inBlockquote) blockquote?.text else null)
|
|
375
|
+
if (nodeType == "paragraph") {
|
|
376
|
+
style = style.mergedWith(paragraph)
|
|
377
|
+
if (paragraph?.lineHeight == null) {
|
|
378
|
+
style = style.copy(lineHeight = null)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (nodeType == "codeBlock") {
|
|
382
|
+
style = style.mergedWith(codeBlock?.text)
|
|
383
|
+
}
|
|
384
|
+
style = style.mergedWith(headings[nodeType])
|
|
385
|
+
return style
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private fun parseColor(raw: String?): Int? {
|
|
390
|
+
val value = raw?.trim()?.lowercase() ?: return null
|
|
391
|
+
if (value.isEmpty()) return null
|
|
392
|
+
|
|
393
|
+
when (value) {
|
|
394
|
+
"clear", "transparent" -> return Color.TRANSPARENT
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
parseCssHexColor(value)?.let { return it }
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
return Color.parseColor(value)
|
|
401
|
+
} catch (_: IllegalArgumentException) {
|
|
402
|
+
// Fall through to rgb()/rgba() parsing.
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return when {
|
|
406
|
+
value.startsWith("rgb(") && value.endsWith(")") -> {
|
|
407
|
+
val parts = value.removePrefix("rgb(").removeSuffix(")")
|
|
408
|
+
.split(',')
|
|
409
|
+
.map { it.trim() }
|
|
410
|
+
if (parts.size != 3) return null
|
|
411
|
+
val red = parts[0].toDoubleOrNull() ?: return null
|
|
412
|
+
val green = parts[1].toDoubleOrNull() ?: return null
|
|
413
|
+
val blue = parts[2].toDoubleOrNull() ?: return null
|
|
414
|
+
Color.argb(255, red.toInt(), green.toInt(), blue.toInt())
|
|
415
|
+
}
|
|
416
|
+
value.startsWith("rgba(") && value.endsWith(")") -> {
|
|
417
|
+
val parts = value.removePrefix("rgba(").removeSuffix(")")
|
|
418
|
+
.split(',')
|
|
419
|
+
.map { it.trim() }
|
|
420
|
+
if (parts.size != 4) return null
|
|
421
|
+
val red = parts[0].toDoubleOrNull() ?: return null
|
|
422
|
+
val green = parts[1].toDoubleOrNull() ?: return null
|
|
423
|
+
val blue = parts[2].toDoubleOrNull() ?: return null
|
|
424
|
+
val alpha = parts[3].toDoubleOrNull() ?: return null
|
|
425
|
+
Color.argb((alpha * 255f).toInt(), red.toInt(), green.toInt(), blue.toInt())
|
|
426
|
+
}
|
|
427
|
+
else -> null
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private fun parseCssHexColor(value: String): Int? {
|
|
432
|
+
if (!value.startsWith("#")) return null
|
|
433
|
+
val hex = value.removePrefix("#")
|
|
434
|
+
|
|
435
|
+
return when (hex.length) {
|
|
436
|
+
3 -> {
|
|
437
|
+
val red = "${hex[0]}${hex[0]}".toIntOrNull(16) ?: return null
|
|
438
|
+
val green = "${hex[1]}${hex[1]}".toIntOrNull(16) ?: return null
|
|
439
|
+
val blue = "${hex[2]}${hex[2]}".toIntOrNull(16) ?: return null
|
|
440
|
+
Color.argb(255, red, green, blue)
|
|
441
|
+
}
|
|
442
|
+
4 -> {
|
|
443
|
+
val red = "${hex[0]}${hex[0]}".toIntOrNull(16) ?: return null
|
|
444
|
+
val green = "${hex[1]}${hex[1]}".toIntOrNull(16) ?: return null
|
|
445
|
+
val blue = "${hex[2]}${hex[2]}".toIntOrNull(16) ?: return null
|
|
446
|
+
val alpha = "${hex[3]}${hex[3]}".toIntOrNull(16) ?: return null
|
|
447
|
+
Color.argb(alpha, red, green, blue)
|
|
448
|
+
}
|
|
449
|
+
6 -> {
|
|
450
|
+
val red = hex.substring(0, 2).toIntOrNull(16) ?: return null
|
|
451
|
+
val green = hex.substring(2, 4).toIntOrNull(16) ?: return null
|
|
452
|
+
val blue = hex.substring(4, 6).toIntOrNull(16) ?: return null
|
|
453
|
+
Color.argb(255, red, green, blue)
|
|
454
|
+
}
|
|
455
|
+
8 -> {
|
|
456
|
+
val red = hex.substring(0, 2).toIntOrNull(16) ?: return null
|
|
457
|
+
val green = hex.substring(2, 4).toIntOrNull(16) ?: return null
|
|
458
|
+
val blue = hex.substring(4, 6).toIntOrNull(16) ?: return null
|
|
459
|
+
val alpha = hex.substring(6, 8).toIntOrNull(16) ?: return null
|
|
460
|
+
Color.argb(alpha, red, green, blue)
|
|
461
|
+
}
|
|
462
|
+
else -> null
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private fun JSONObject.optNullableString(key: String): String? {
|
|
467
|
+
if (!has(key) || isNull(key)) return null
|
|
468
|
+
return optString(key).takeUnless { it == "null" }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private fun JSONObject.optNullableFloat(key: String): Float? {
|
|
472
|
+
if (!has(key) || isNull(key)) return null
|
|
473
|
+
return optDouble(key).takeIf { !it.isNaN() }?.toFloat()
|
|
474
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package com.openeditor.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
|
+
}
|