@apollohg/react-native-prose-editor 0.3.0 → 0.4.1
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/README.md +18 -0
- package/android/build.gradle +23 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +515 -39
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +58 -28
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +232 -62
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
- package/dist/EditorToolbar.d.ts +26 -6
- package/dist/EditorToolbar.js +299 -65
- package/dist/NativeEditorBridge.d.ts +40 -1
- package/dist/NativeEditorBridge.js +184 -90
- package/dist/NativeRichTextEditor.d.ts +5 -1
- package/dist/NativeRichTextEditor.js +201 -78
- package/dist/YjsCollaboration.d.ts +2 -0
- package/dist/YjsCollaboration.js +142 -20
- package/dist/index.d.ts +1 -1
- package/dist/schemas.js +12 -0
- package/dist/useNativeEditor.d.ts +2 -0
- package/dist/useNativeEditor.js +7 -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 +3 -3
- package/ios/Generated_editor_core.swift +87 -0
- package/ios/NativeEditorExpoView.swift +488 -178
- package/ios/NativeEditorModule.swift +25 -0
- package/ios/PositionBridge.swift +310 -75
- package/ios/RenderBridge.swift +362 -27
- package/ios/RichTextEditorView.swift +2001 -189
- package/ios/editor_coreFFI/editor_coreFFI.h +55 -0
- package/package.json +11 -2
- 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 +128 -0
|
@@ -65,6 +65,11 @@ class NativeEditorExpoView(
|
|
|
65
65
|
private var addons = NativeEditorAddons(null)
|
|
66
66
|
private var mentionQueryState: MentionQueryState? = null
|
|
67
67
|
private var lastMentionEventJson: String? = null
|
|
68
|
+
private var lastThemeJson: String? = null
|
|
69
|
+
private var lastAddonsJson: String? = null
|
|
70
|
+
private var lastRemoteSelectionsJson: String? = null
|
|
71
|
+
private var lastToolbarItemsJson: String? = null
|
|
72
|
+
private var lastToolbarFrameJson: String? = null
|
|
68
73
|
private var toolbarState = NativeToolbarState.empty
|
|
69
74
|
private var showsToolbar = true
|
|
70
75
|
private var toolbarPlacement = ToolbarPlacement.KEYBOARD
|
|
@@ -110,6 +115,8 @@ class NativeEditorExpoView(
|
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
fun setThemeJson(themeJson: String?) {
|
|
118
|
+
if (lastThemeJson == themeJson) return
|
|
119
|
+
lastThemeJson = themeJson
|
|
113
120
|
val theme = EditorTheme.fromJson(themeJson)
|
|
114
121
|
richTextView.applyTheme(theme)
|
|
115
122
|
keyboardToolbarView.applyTheme(theme?.toolbar)
|
|
@@ -141,12 +148,16 @@ class NativeEditorExpoView(
|
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
fun setAddonsJson(addonsJson: String?) {
|
|
151
|
+
if (lastAddonsJson == addonsJson) return
|
|
152
|
+
lastAddonsJson = addonsJson
|
|
144
153
|
addons = NativeEditorAddons.fromJson(addonsJson)
|
|
145
154
|
keyboardToolbarView.applyMentionTheme(richTextView.editorEditText.theme?.mentions ?: addons.mentions?.theme)
|
|
146
155
|
refreshMentionQuery()
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
fun setRemoteSelectionsJson(remoteSelectionsJson: String?) {
|
|
159
|
+
if (lastRemoteSelectionsJson == remoteSelectionsJson) return
|
|
160
|
+
lastRemoteSelectionsJson = remoteSelectionsJson
|
|
150
161
|
richTextView.setRemoteSelections(
|
|
151
162
|
RemoteSelectionDecoration.fromJson(context, remoteSelectionsJson)
|
|
152
163
|
)
|
|
@@ -175,10 +186,14 @@ class NativeEditorExpoView(
|
|
|
175
186
|
}
|
|
176
187
|
|
|
177
188
|
fun setToolbarItemsJson(toolbarItemsJson: String?) {
|
|
189
|
+
if (lastToolbarItemsJson == toolbarItemsJson) return
|
|
190
|
+
lastToolbarItemsJson = toolbarItemsJson
|
|
178
191
|
keyboardToolbarView.setItems(NativeToolbarItem.fromJson(toolbarItemsJson))
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
fun setToolbarFrameJson(toolbarFrameJson: String?) {
|
|
195
|
+
if (lastToolbarFrameJson == toolbarFrameJson) return
|
|
196
|
+
lastToolbarFrameJson = toolbarFrameJson
|
|
182
197
|
if (toolbarFrameJson.isNullOrBlank()) {
|
|
183
198
|
toolbarFrameInWindow = null
|
|
184
199
|
return
|
|
@@ -260,31 +275,44 @@ class NativeEditorExpoView(
|
|
|
260
275
|
if (heightBehavior != EditorHeightBehavior.AUTO_GROW) return
|
|
261
276
|
val editText = richTextView.editorEditText
|
|
262
277
|
val resolvedEditHeight = editText.resolveAutoGrowHeight()
|
|
278
|
+
val resolvedContainerHeight =
|
|
279
|
+
resolvedEditHeight +
|
|
280
|
+
richTextView.paddingTop +
|
|
281
|
+
richTextView.paddingBottom +
|
|
282
|
+
paddingTop +
|
|
283
|
+
paddingBottom
|
|
263
284
|
val contentHeight = (
|
|
264
285
|
when {
|
|
265
286
|
editText.isLaidOut && (editText.layout?.height ?: 0) > 0 -> {
|
|
266
|
-
(
|
|
267
|
-
editText.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
287
|
+
maxOf(
|
|
288
|
+
(editText.layout?.height ?: 0) +
|
|
289
|
+
editText.compoundPaddingTop +
|
|
290
|
+
editText.compoundPaddingBottom +
|
|
291
|
+
richTextView.paddingTop +
|
|
292
|
+
richTextView.paddingBottom +
|
|
293
|
+
paddingTop +
|
|
294
|
+
paddingBottom,
|
|
295
|
+
resolvedContainerHeight
|
|
296
|
+
)
|
|
271
297
|
}
|
|
272
298
|
richTextView.measuredHeight > 0 -> {
|
|
273
|
-
|
|
299
|
+
maxOf(
|
|
300
|
+
richTextView.measuredHeight + paddingTop + paddingBottom,
|
|
301
|
+
resolvedContainerHeight
|
|
302
|
+
)
|
|
274
303
|
}
|
|
275
304
|
editText.measuredHeight > 0 -> {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
maxOf(
|
|
306
|
+
editText.measuredHeight +
|
|
307
|
+
richTextView.paddingTop +
|
|
308
|
+
richTextView.paddingBottom +
|
|
309
|
+
paddingTop +
|
|
310
|
+
paddingBottom,
|
|
311
|
+
resolvedContainerHeight
|
|
312
|
+
)
|
|
281
313
|
}
|
|
282
314
|
else -> {
|
|
283
|
-
|
|
284
|
-
richTextView.paddingTop +
|
|
285
|
-
richTextView.paddingBottom +
|
|
286
|
-
paddingTop +
|
|
287
|
-
paddingBottom
|
|
315
|
+
resolvedContainerHeight
|
|
288
316
|
}
|
|
289
317
|
}
|
|
290
318
|
).coerceAtLeast(0)
|
|
@@ -304,20 +332,20 @@ class NativeEditorExpoView(
|
|
|
304
332
|
if (android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) {
|
|
305
333
|
apply.run()
|
|
306
334
|
} else {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
apply.run()
|
|
310
|
-
latch.countDown()
|
|
335
|
+
if (!post(apply)) {
|
|
336
|
+
richTextView.post(apply)
|
|
311
337
|
}
|
|
312
|
-
latch.await()
|
|
313
338
|
}
|
|
314
339
|
}
|
|
315
340
|
|
|
316
341
|
override fun onSelectionChanged(anchor: Int, head: Int) {
|
|
317
|
-
refreshToolbarStateFromEditorSelection()
|
|
342
|
+
val stateJson = refreshToolbarStateFromEditorSelection()
|
|
318
343
|
refreshMentionQuery()
|
|
319
344
|
richTextView.refreshRemoteSelections()
|
|
320
|
-
val event =
|
|
345
|
+
val event = mutableMapOf<String, Any>("anchor" to anchor, "head" to head)
|
|
346
|
+
if (stateJson != null) {
|
|
347
|
+
event["stateJson"] = stateJson
|
|
348
|
+
}
|
|
321
349
|
onSelectionChange(event)
|
|
322
350
|
}
|
|
323
351
|
|
|
@@ -541,13 +569,13 @@ class NativeEditorExpoView(
|
|
|
541
569
|
clearMentionQueryState()
|
|
542
570
|
}
|
|
543
571
|
|
|
544
|
-
private fun refreshToolbarStateFromEditorSelection() {
|
|
545
|
-
if (richTextView.editorId == 0L) return
|
|
546
|
-
val
|
|
547
|
-
|
|
548
|
-
) ?: return
|
|
572
|
+
private fun refreshToolbarStateFromEditorSelection(): String? {
|
|
573
|
+
if (richTextView.editorId == 0L) return null
|
|
574
|
+
val stateJson = editorGetSelectionState(richTextView.editorId.toULong())
|
|
575
|
+
val state = NativeToolbarState.fromUpdateJson(stateJson) ?: return null
|
|
549
576
|
toolbarState = state
|
|
550
577
|
keyboardToolbarView.applyState(state)
|
|
578
|
+
return stateJson
|
|
551
579
|
}
|
|
552
580
|
|
|
553
581
|
private fun ensureKeyboardToolbarAttached() {
|
|
@@ -642,6 +670,7 @@ class NativeEditorExpoView(
|
|
|
642
670
|
private fun handleToolbarItemPress(item: NativeToolbarItem) {
|
|
643
671
|
when (item.type) {
|
|
644
672
|
ToolbarItemKind.mark -> item.mark?.let { richTextView.editorEditText.performToolbarToggleMark(it) }
|
|
673
|
+
ToolbarItemKind.heading -> item.headingLevel?.let { richTextView.editorEditText.performToolbarToggleHeading(it) }
|
|
645
674
|
ToolbarItemKind.blockquote -> richTextView.editorEditText.performToolbarToggleBlockquote()
|
|
646
675
|
ToolbarItemKind.list -> item.listType?.name?.let { handleListToggle(it) }
|
|
647
676
|
ToolbarItemKind.command -> when (item.command) {
|
|
@@ -653,6 +682,7 @@ class NativeEditorExpoView(
|
|
|
653
682
|
}
|
|
654
683
|
ToolbarItemKind.node -> item.nodeType?.let { richTextView.editorEditText.performToolbarInsertNode(it) }
|
|
655
684
|
ToolbarItemKind.action -> item.key?.let { onToolbarAction(mapOf("key" to it)) }
|
|
685
|
+
ToolbarItemKind.group -> Unit
|
|
656
686
|
ToolbarItemKind.separator -> Unit
|
|
657
687
|
}
|
|
658
688
|
}
|
|
@@ -63,6 +63,9 @@ class NativeEditorModule : Module() {
|
|
|
63
63
|
Function("editorGetJson") { id: Int ->
|
|
64
64
|
editorGetJson(id.toULong())
|
|
65
65
|
}
|
|
66
|
+
Function("editorGetContentSnapshot") { id: Int ->
|
|
67
|
+
editorGetContentSnapshot(id.toULong())
|
|
68
|
+
}
|
|
66
69
|
|
|
67
70
|
Function("editorInsertText") { id: Int, pos: Int, text: String ->
|
|
68
71
|
editorInsertText(id.toULong(), pos.toUInt(), text)
|
|
@@ -169,6 +172,19 @@ class NativeEditorModule : Module() {
|
|
|
169
172
|
scalarHead.toUInt()
|
|
170
173
|
)
|
|
171
174
|
}
|
|
175
|
+
Function(
|
|
176
|
+
"editorToggleHeadingAtSelectionScalar"
|
|
177
|
+
) { id: Int, scalarAnchor: Int, scalarHead: Int, level: Int ->
|
|
178
|
+
if (level !in 1..6) {
|
|
179
|
+
return@Function "{\"error\":\"invalid heading level\"}"
|
|
180
|
+
}
|
|
181
|
+
editorToggleHeadingAtSelectionScalar(
|
|
182
|
+
id.toULong(),
|
|
183
|
+
scalarAnchor.toUInt(),
|
|
184
|
+
scalarHead.toUInt(),
|
|
185
|
+
level.toUByte()
|
|
186
|
+
)
|
|
187
|
+
}
|
|
172
188
|
Function(
|
|
173
189
|
"editorWrapInListAtSelectionScalar"
|
|
174
190
|
) { id: Int, scalarAnchor: Int, scalarHead: Int, listType: String ->
|
|
@@ -229,6 +245,12 @@ class NativeEditorModule : Module() {
|
|
|
229
245
|
Function("editorToggleBlockquote") { id: Int ->
|
|
230
246
|
editorToggleBlockquote(id.toULong())
|
|
231
247
|
}
|
|
248
|
+
Function("editorToggleHeading") { id: Int, level: Int ->
|
|
249
|
+
if (level !in 1..6) {
|
|
250
|
+
return@Function "{\"error\":\"invalid heading level\"}"
|
|
251
|
+
}
|
|
252
|
+
editorToggleHeading(id.toULong(), level.toUByte())
|
|
253
|
+
}
|
|
232
254
|
|
|
233
255
|
Function("editorSetSelection") { id: Int, anchor: Int, head: Int ->
|
|
234
256
|
editorSetSelection(id.toULong(), anchor.toUInt(), head.toUInt())
|
|
@@ -239,6 +261,9 @@ class NativeEditorModule : Module() {
|
|
|
239
261
|
Function("editorGetSelection") { id: Int ->
|
|
240
262
|
editorGetSelection(id.toULong())
|
|
241
263
|
}
|
|
264
|
+
Function("editorGetSelectionState") { id: Int ->
|
|
265
|
+
editorGetSelectionState(id.toULong())
|
|
266
|
+
}
|
|
242
267
|
Function("editorDocToScalar") { id: Int, docPos: Int ->
|
|
243
268
|
editorDocToScalar(id.toULong(), docPos.toUInt()).toInt()
|
|
244
269
|
}
|
|
@@ -12,6 +12,7 @@ import android.view.ViewOutlineProvider
|
|
|
12
12
|
import android.widget.HorizontalScrollView
|
|
13
13
|
import android.widget.LinearLayout
|
|
14
14
|
import androidx.appcompat.widget.AppCompatButton
|
|
15
|
+
import androidx.appcompat.widget.PopupMenu
|
|
15
16
|
import androidx.appcompat.widget.AppCompatTextView
|
|
16
17
|
import androidx.appcompat.content.res.AppCompatResources
|
|
17
18
|
import androidx.core.view.setPadding
|
|
@@ -100,6 +101,12 @@ internal enum class ToolbarDefaultIconId {
|
|
|
100
101
|
strike,
|
|
101
102
|
link,
|
|
102
103
|
image,
|
|
104
|
+
h1,
|
|
105
|
+
h2,
|
|
106
|
+
h3,
|
|
107
|
+
h4,
|
|
108
|
+
h5,
|
|
109
|
+
h6,
|
|
103
110
|
blockquote,
|
|
104
111
|
bulletList,
|
|
105
112
|
orderedList,
|
|
@@ -113,14 +120,21 @@ internal enum class ToolbarDefaultIconId {
|
|
|
113
120
|
|
|
114
121
|
internal enum class ToolbarItemKind {
|
|
115
122
|
mark,
|
|
123
|
+
heading,
|
|
116
124
|
blockquote,
|
|
117
125
|
list,
|
|
118
126
|
command,
|
|
119
127
|
node,
|
|
120
128
|
action,
|
|
129
|
+
group,
|
|
121
130
|
separator,
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
internal enum class ToolbarGroupPresentation {
|
|
134
|
+
expand,
|
|
135
|
+
menu,
|
|
136
|
+
}
|
|
137
|
+
|
|
124
138
|
internal data class NativeToolbarIcon(
|
|
125
139
|
val defaultId: ToolbarDefaultIconId? = null,
|
|
126
140
|
val glyphText: String? = null,
|
|
@@ -135,6 +149,12 @@ internal data class NativeToolbarIcon(
|
|
|
135
149
|
ToolbarDefaultIconId.strike to "S",
|
|
136
150
|
ToolbarDefaultIconId.link to "🔗",
|
|
137
151
|
ToolbarDefaultIconId.image to "🖼",
|
|
152
|
+
ToolbarDefaultIconId.h1 to "H1",
|
|
153
|
+
ToolbarDefaultIconId.h2 to "H2",
|
|
154
|
+
ToolbarDefaultIconId.h3 to "H3",
|
|
155
|
+
ToolbarDefaultIconId.h4 to "H4",
|
|
156
|
+
ToolbarDefaultIconId.h5 to "H5",
|
|
157
|
+
ToolbarDefaultIconId.h6 to "H6",
|
|
138
158
|
ToolbarDefaultIconId.blockquote to "❝",
|
|
139
159
|
ToolbarDefaultIconId.bulletList to "•≡",
|
|
140
160
|
ToolbarDefaultIconId.orderedList to "1.",
|
|
@@ -159,6 +179,12 @@ internal data class NativeToolbarIcon(
|
|
|
159
179
|
ToolbarDefaultIconId.outdentList to "format-indent-decrease",
|
|
160
180
|
ToolbarDefaultIconId.lineBreak to "keyboard-return",
|
|
161
181
|
ToolbarDefaultIconId.horizontalRule to "horizontal-rule",
|
|
182
|
+
ToolbarDefaultIconId.h1 to "title",
|
|
183
|
+
ToolbarDefaultIconId.h2 to "title",
|
|
184
|
+
ToolbarDefaultIconId.h3 to "title",
|
|
185
|
+
ToolbarDefaultIconId.h4 to "title",
|
|
186
|
+
ToolbarDefaultIconId.h5 to "title",
|
|
187
|
+
ToolbarDefaultIconId.h6 to "title",
|
|
162
188
|
ToolbarDefaultIconId.undo to "undo",
|
|
163
189
|
ToolbarDefaultIconId.redo to "redo"
|
|
164
190
|
)
|
|
@@ -284,11 +310,15 @@ internal data class NativeToolbarItem(
|
|
|
284
310
|
val label: String? = null,
|
|
285
311
|
val icon: NativeToolbarIcon? = null,
|
|
286
312
|
val mark: String? = null,
|
|
313
|
+
val headingLevel: Int? = null,
|
|
287
314
|
val listType: ToolbarListType? = null,
|
|
288
315
|
val command: ToolbarCommand? = null,
|
|
289
316
|
val nodeType: String? = null,
|
|
290
317
|
val isActive: Boolean = false,
|
|
291
|
-
val isDisabled: Boolean = false
|
|
318
|
+
val isDisabled: Boolean = false,
|
|
319
|
+
val presentation: ToolbarGroupPresentation? = null,
|
|
320
|
+
val items: List<NativeToolbarItem> = emptyList(),
|
|
321
|
+
val parentGroupKey: String? = null
|
|
292
322
|
) {
|
|
293
323
|
companion object {
|
|
294
324
|
val defaults = listOf(
|
|
@@ -309,6 +339,105 @@ internal data class NativeToolbarItem(
|
|
|
309
339
|
NativeToolbarItem(ToolbarItemKind.command, label = "Redo", icon = NativeToolbarIcon(defaultId = ToolbarDefaultIconId.redo), command = ToolbarCommand.redo)
|
|
310
340
|
)
|
|
311
341
|
|
|
342
|
+
private fun parseItem(
|
|
343
|
+
rawItem: JSONObject,
|
|
344
|
+
allowGroup: Boolean = true,
|
|
345
|
+
allowSeparator: Boolean = true
|
|
346
|
+
): NativeToolbarItem? {
|
|
347
|
+
val type = runCatching {
|
|
348
|
+
ToolbarItemKind.valueOf(rawItem.getString("type"))
|
|
349
|
+
}.getOrNull() ?: return null
|
|
350
|
+
val key = rawItem.optNullableString("key")
|
|
351
|
+
return when (type) {
|
|
352
|
+
ToolbarItemKind.separator -> {
|
|
353
|
+
if (!allowSeparator) {
|
|
354
|
+
null
|
|
355
|
+
} else {
|
|
356
|
+
NativeToolbarItem(type = type, key = key)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
ToolbarItemKind.mark -> {
|
|
360
|
+
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: return null
|
|
361
|
+
val mark = rawItem.optNullableString("mark") ?: return null
|
|
362
|
+
val label = rawItem.optNullableString("label") ?: return null
|
|
363
|
+
NativeToolbarItem(type, key, label, icon, mark = mark)
|
|
364
|
+
}
|
|
365
|
+
ToolbarItemKind.heading -> {
|
|
366
|
+
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: return null
|
|
367
|
+
val level = rawItem.optInt("level", -1)
|
|
368
|
+
if (level !in 1..6) return null
|
|
369
|
+
val label = rawItem.optNullableString("label") ?: return null
|
|
370
|
+
NativeToolbarItem(type, key, label, icon, headingLevel = level)
|
|
371
|
+
}
|
|
372
|
+
ToolbarItemKind.blockquote -> {
|
|
373
|
+
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: return null
|
|
374
|
+
val label = rawItem.optNullableString("label") ?: return null
|
|
375
|
+
NativeToolbarItem(type, key, label, icon)
|
|
376
|
+
}
|
|
377
|
+
ToolbarItemKind.list -> {
|
|
378
|
+
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: return null
|
|
379
|
+
val listType = runCatching {
|
|
380
|
+
ToolbarListType.valueOf(rawItem.getString("listType"))
|
|
381
|
+
}.getOrNull() ?: return null
|
|
382
|
+
val label = rawItem.optNullableString("label") ?: return null
|
|
383
|
+
NativeToolbarItem(type, key, label, icon, listType = listType)
|
|
384
|
+
}
|
|
385
|
+
ToolbarItemKind.command -> {
|
|
386
|
+
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: return null
|
|
387
|
+
val command = runCatching {
|
|
388
|
+
ToolbarCommand.valueOf(rawItem.getString("command"))
|
|
389
|
+
}.getOrNull() ?: return null
|
|
390
|
+
val label = rawItem.optNullableString("label") ?: return null
|
|
391
|
+
NativeToolbarItem(type, key, label, icon, command = command)
|
|
392
|
+
}
|
|
393
|
+
ToolbarItemKind.node -> {
|
|
394
|
+
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: return null
|
|
395
|
+
val nodeType = rawItem.optNullableString("nodeType") ?: return null
|
|
396
|
+
val label = rawItem.optNullableString("label") ?: return null
|
|
397
|
+
NativeToolbarItem(type, key, label, icon, nodeType = nodeType)
|
|
398
|
+
}
|
|
399
|
+
ToolbarItemKind.action -> {
|
|
400
|
+
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: return null
|
|
401
|
+
val keyValue = rawItem.optNullableString("key") ?: return null
|
|
402
|
+
val label = rawItem.optNullableString("label") ?: return null
|
|
403
|
+
NativeToolbarItem(
|
|
404
|
+
type = type,
|
|
405
|
+
key = keyValue,
|
|
406
|
+
label = label,
|
|
407
|
+
icon = icon,
|
|
408
|
+
isActive = rawItem.optBoolean("isActive", false),
|
|
409
|
+
isDisabled = rawItem.optBoolean("isDisabled", false)
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
ToolbarItemKind.group -> {
|
|
413
|
+
if (!allowGroup) return null
|
|
414
|
+
val keyValue = rawItem.optNullableString("key") ?: return null
|
|
415
|
+
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: return null
|
|
416
|
+
val label = rawItem.optNullableString("label") ?: return null
|
|
417
|
+
val presentation = rawItem.optNullableString("presentation")?.let {
|
|
418
|
+
runCatching { ToolbarGroupPresentation.valueOf(it) }.getOrNull()
|
|
419
|
+
} ?: ToolbarGroupPresentation.expand
|
|
420
|
+
val rawChildren = rawItem.optJSONArray("items") ?: return null
|
|
421
|
+
val children = mutableListOf<NativeToolbarItem>()
|
|
422
|
+
for (childIndex in 0 until rawChildren.length()) {
|
|
423
|
+
val rawChild = rawChildren.optJSONObject(childIndex) ?: continue
|
|
424
|
+
parseItem(rawChild, allowGroup = false, allowSeparator = false)?.let {
|
|
425
|
+
children += it
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (children.isEmpty()) return null
|
|
429
|
+
NativeToolbarItem(
|
|
430
|
+
type = type,
|
|
431
|
+
key = keyValue,
|
|
432
|
+
label = label,
|
|
433
|
+
icon = icon,
|
|
434
|
+
presentation = presentation,
|
|
435
|
+
items = children
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
312
441
|
fun fromJson(json: String?): List<NativeToolbarItem> {
|
|
313
442
|
if (json.isNullOrBlank()) return defaults
|
|
314
443
|
val rawArray = try {
|
|
@@ -319,61 +448,7 @@ internal data class NativeToolbarItem(
|
|
|
319
448
|
val parsed = mutableListOf<NativeToolbarItem>()
|
|
320
449
|
for (index in 0 until rawArray.length()) {
|
|
321
450
|
val rawItem = rawArray.optJSONObject(index) ?: continue
|
|
322
|
-
|
|
323
|
-
ToolbarItemKind.valueOf(rawItem.getString("type"))
|
|
324
|
-
}.getOrNull() ?: continue
|
|
325
|
-
val key = rawItem.optNullableString("key")
|
|
326
|
-
when (type) {
|
|
327
|
-
ToolbarItemKind.separator -> parsed.add(NativeToolbarItem(type = type, key = key))
|
|
328
|
-
ToolbarItemKind.mark -> {
|
|
329
|
-
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: continue
|
|
330
|
-
val mark = rawItem.optNullableString("mark") ?: continue
|
|
331
|
-
val label = rawItem.optNullableString("label") ?: continue
|
|
332
|
-
parsed.add(NativeToolbarItem(type, key, label, icon, mark = mark))
|
|
333
|
-
}
|
|
334
|
-
ToolbarItemKind.blockquote -> {
|
|
335
|
-
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: continue
|
|
336
|
-
val label = rawItem.optNullableString("label") ?: continue
|
|
337
|
-
parsed.add(NativeToolbarItem(type, key, label, icon))
|
|
338
|
-
}
|
|
339
|
-
ToolbarItemKind.list -> {
|
|
340
|
-
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: continue
|
|
341
|
-
val listType = runCatching {
|
|
342
|
-
ToolbarListType.valueOf(rawItem.getString("listType"))
|
|
343
|
-
}.getOrNull() ?: continue
|
|
344
|
-
val label = rawItem.optNullableString("label") ?: continue
|
|
345
|
-
parsed.add(NativeToolbarItem(type, key, label, icon, listType = listType))
|
|
346
|
-
}
|
|
347
|
-
ToolbarItemKind.command -> {
|
|
348
|
-
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: continue
|
|
349
|
-
val command = runCatching {
|
|
350
|
-
ToolbarCommand.valueOf(rawItem.getString("command"))
|
|
351
|
-
}.getOrNull() ?: continue
|
|
352
|
-
val label = rawItem.optNullableString("label") ?: continue
|
|
353
|
-
parsed.add(NativeToolbarItem(type, key, label, icon, command = command))
|
|
354
|
-
}
|
|
355
|
-
ToolbarItemKind.node -> {
|
|
356
|
-
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: continue
|
|
357
|
-
val nodeType = rawItem.optNullableString("nodeType") ?: continue
|
|
358
|
-
val label = rawItem.optNullableString("label") ?: continue
|
|
359
|
-
parsed.add(NativeToolbarItem(type, key, label, icon, nodeType = nodeType))
|
|
360
|
-
}
|
|
361
|
-
ToolbarItemKind.action -> {
|
|
362
|
-
val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: continue
|
|
363
|
-
val keyValue = rawItem.optNullableString("key") ?: continue
|
|
364
|
-
val label = rawItem.optNullableString("label") ?: continue
|
|
365
|
-
parsed.add(
|
|
366
|
-
NativeToolbarItem(
|
|
367
|
-
type = type,
|
|
368
|
-
key = keyValue,
|
|
369
|
-
label = label,
|
|
370
|
-
icon = icon,
|
|
371
|
-
isActive = rawItem.optBoolean("isActive", false),
|
|
372
|
-
isDisabled = rawItem.optBoolean("isDisabled", false)
|
|
373
|
-
)
|
|
374
|
-
)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
451
|
+
parseItem(rawItem)?.let { parsed += it }
|
|
377
452
|
}
|
|
378
453
|
return parsed.ifEmpty { defaults }
|
|
379
454
|
}
|
|
@@ -406,6 +481,8 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
406
481
|
private var state: NativeToolbarState = NativeToolbarState.empty
|
|
407
482
|
private var items: List<NativeToolbarItem> = NativeToolbarItem.defaults
|
|
408
483
|
private var mentionSuggestions: List<NativeMentionSuggestion> = emptyList()
|
|
484
|
+
private var expandedGroupKey: String? = null
|
|
485
|
+
private var rebuildGeneration: Int = 0
|
|
409
486
|
private val bindings = mutableListOf<ButtonBinding>()
|
|
410
487
|
private val separators = mutableListOf<View>()
|
|
411
488
|
private val mentionChips = mutableListOf<MentionSuggestionChipView>()
|
|
@@ -443,11 +520,14 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
443
520
|
contentRow,
|
|
444
521
|
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
445
522
|
)
|
|
446
|
-
rebuildContent()
|
|
523
|
+
rebuildContent(preserveScrollPosition = false)
|
|
447
524
|
}
|
|
448
525
|
|
|
449
526
|
fun setItems(items: List<NativeToolbarItem>) {
|
|
450
527
|
this.items = compactItems(items)
|
|
528
|
+
if (expandedGroupKey != null && !containsExpandableGroup(this.items, expandedGroupKey)) {
|
|
529
|
+
expandedGroupKey = null
|
|
530
|
+
}
|
|
451
531
|
if (!isShowingMentionSuggestions) {
|
|
452
532
|
rebuildContent()
|
|
453
533
|
}
|
|
@@ -491,7 +571,7 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
491
571
|
fun setMentionSuggestions(suggestions: List<NativeMentionSuggestion>): Boolean {
|
|
492
572
|
val hadSuggestions = isShowingMentionSuggestions
|
|
493
573
|
mentionSuggestions = suggestions.take(8)
|
|
494
|
-
rebuildContent()
|
|
574
|
+
rebuildContent(preserveScrollPosition = hadSuggestions == isShowingMentionSuggestions)
|
|
495
575
|
return hadSuggestions != isShowingMentionSuggestions
|
|
496
576
|
}
|
|
497
577
|
|
|
@@ -502,6 +582,8 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
502
582
|
internal fun buttonAtForTesting(index: Int): AppCompatButton? =
|
|
503
583
|
bindings.getOrNull(index)?.button
|
|
504
584
|
|
|
585
|
+
internal fun buttonCountForTesting(): Int = bindings.size
|
|
586
|
+
|
|
505
587
|
internal fun buttonBackgroundColorAtForTesting(index: Int): Int? =
|
|
506
588
|
bindings.getOrNull(index)?.button?.let { buttonBackgroundColors[it] }
|
|
507
589
|
|
|
@@ -511,7 +593,9 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
511
593
|
internal fun separatorAtForTesting(index: Int): View? =
|
|
512
594
|
separators.getOrNull(index)
|
|
513
595
|
|
|
514
|
-
private fun rebuildContent() {
|
|
596
|
+
private fun rebuildContent(preserveScrollPosition: Boolean = true) {
|
|
597
|
+
val targetScrollX = if (preserveScrollPosition) scrollX else 0
|
|
598
|
+
val generation = ++rebuildGeneration
|
|
515
599
|
bindings.clear()
|
|
516
600
|
separators.clear()
|
|
517
601
|
mentionChips.clear()
|
|
@@ -525,11 +609,17 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
525
609
|
|
|
526
610
|
updateChrome()
|
|
527
611
|
applyState(state)
|
|
528
|
-
|
|
612
|
+
post {
|
|
613
|
+
if (generation != rebuildGeneration) return@post
|
|
614
|
+
val contentWidth = getChildAt(0)?.width ?: 0
|
|
615
|
+
val viewportWidth = (width - paddingLeft - paddingRight).coerceAtLeast(0)
|
|
616
|
+
val maxScrollX = (contentWidth - viewportWidth).coerceAtLeast(0)
|
|
617
|
+
scrollTo(targetScrollX.coerceIn(0, maxScrollX), 0)
|
|
618
|
+
}
|
|
529
619
|
}
|
|
530
620
|
|
|
531
621
|
private fun rebuildButtons() {
|
|
532
|
-
for (item in
|
|
622
|
+
for (item in visibleItems()) {
|
|
533
623
|
if (item.type == ToolbarItemKind.separator) {
|
|
534
624
|
val separator = View(context)
|
|
535
625
|
configureSeparator(separator)
|
|
@@ -548,7 +638,18 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
548
638
|
isAllCaps = false
|
|
549
639
|
includeFontPadding = false
|
|
550
640
|
contentDescription = item.label
|
|
551
|
-
setOnClickListener {
|
|
641
|
+
setOnClickListener {
|
|
642
|
+
when (item.type) {
|
|
643
|
+
ToolbarItemKind.group -> handleGroupButtonPress(this, item)
|
|
644
|
+
else -> {
|
|
645
|
+
onPressItem?.invoke(item.copy(parentGroupKey = null))
|
|
646
|
+
if (item.parentGroupKey != null && expandedGroupKey == item.parentGroupKey) {
|
|
647
|
+
expandedGroupKey = null
|
|
648
|
+
rebuildContent()
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
552
653
|
elevation = 0f
|
|
553
654
|
translationZ = 0f
|
|
554
655
|
stateListAnimator = null
|
|
@@ -594,6 +695,59 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
594
695
|
}
|
|
595
696
|
}
|
|
596
697
|
|
|
698
|
+
private fun visibleItems(): List<NativeToolbarItem> {
|
|
699
|
+
val visible = mutableListOf<NativeToolbarItem>()
|
|
700
|
+
for (item in compactItems(items)) {
|
|
701
|
+
visible += item
|
|
702
|
+
if (
|
|
703
|
+
item.type == ToolbarItemKind.group &&
|
|
704
|
+
(item.presentation ?: ToolbarGroupPresentation.expand) == ToolbarGroupPresentation.expand &&
|
|
705
|
+
expandedGroupKey == item.key
|
|
706
|
+
) {
|
|
707
|
+
visible += item.items.map { child -> child.copy(parentGroupKey = item.key) }
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return compactItems(visible)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private fun containsExpandableGroup(items: List<NativeToolbarItem>, key: String?): Boolean {
|
|
714
|
+
key ?: return false
|
|
715
|
+
return items.any {
|
|
716
|
+
it.type == ToolbarItemKind.group &&
|
|
717
|
+
it.key == key &&
|
|
718
|
+
(it.presentation ?: ToolbarGroupPresentation.expand) == ToolbarGroupPresentation.expand
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private fun handleGroupButtonPress(anchor: View, item: NativeToolbarItem) {
|
|
723
|
+
if (item.items.isEmpty()) return
|
|
724
|
+
when (item.presentation ?: ToolbarGroupPresentation.expand) {
|
|
725
|
+
ToolbarGroupPresentation.expand -> {
|
|
726
|
+
val key = item.key ?: return
|
|
727
|
+
expandedGroupKey = if (expandedGroupKey == key) null else key
|
|
728
|
+
rebuildContent()
|
|
729
|
+
}
|
|
730
|
+
ToolbarGroupPresentation.menu -> showGroupMenu(anchor, item)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private fun showGroupMenu(anchor: View, item: NativeToolbarItem) {
|
|
735
|
+
val popupMenu = PopupMenu(themedContext, anchor)
|
|
736
|
+
item.items.forEachIndexed { index, child ->
|
|
737
|
+
val (enabled, active) = buttonState(child, state)
|
|
738
|
+
val menuItem = popupMenu.menu.add(0, index, index, child.label ?: child.key ?: "Item")
|
|
739
|
+
menuItem.isEnabled = enabled
|
|
740
|
+
menuItem.isCheckable = true
|
|
741
|
+
menuItem.isChecked = active
|
|
742
|
+
}
|
|
743
|
+
popupMenu.setOnMenuItemClickListener { menuItem ->
|
|
744
|
+
val child = item.items.getOrNull(menuItem.itemId) ?: return@setOnMenuItemClickListener false
|
|
745
|
+
onPressItem?.invoke(child)
|
|
746
|
+
true
|
|
747
|
+
}
|
|
748
|
+
popupMenu.show()
|
|
749
|
+
}
|
|
750
|
+
|
|
597
751
|
private fun updateChrome() {
|
|
598
752
|
val appearance = theme?.appearance ?: EditorToolbarAppearance.CUSTOM
|
|
599
753
|
val cornerRadiusPx = (theme?.resolvedBorderRadius() ?: 0f) * density
|
|
@@ -736,6 +890,13 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
736
890
|
val mark = item.mark.orEmpty()
|
|
737
891
|
Pair(state.allowedMarks.contains(mark), state.marks[mark] == true)
|
|
738
892
|
}
|
|
893
|
+
ToolbarItemKind.heading -> {
|
|
894
|
+
val level = item.headingLevel ?: return Pair(false, false)
|
|
895
|
+
Pair(
|
|
896
|
+
state.commands["toggleHeading$level"] == true,
|
|
897
|
+
state.nodes["h$level"] == true
|
|
898
|
+
)
|
|
899
|
+
}
|
|
739
900
|
ToolbarItemKind.blockquote -> Pair(
|
|
740
901
|
state.commands["toggleBlockquote"] == true,
|
|
741
902
|
state.nodes["blockquote"] == true
|
|
@@ -763,6 +924,15 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
|
|
|
763
924
|
Pair(state.insertableNodes.contains(nodeType), state.nodes[nodeType] == true)
|
|
764
925
|
}
|
|
765
926
|
ToolbarItemKind.action -> Pair(!item.isDisabled, item.isActive)
|
|
927
|
+
ToolbarItemKind.group -> Pair(
|
|
928
|
+
item.items.any { child -> buttonState(child, state).first },
|
|
929
|
+
item.items.any { child -> buttonState(child, state).second } ||
|
|
930
|
+
(
|
|
931
|
+
(item.presentation ?: ToolbarGroupPresentation.expand) ==
|
|
932
|
+
ToolbarGroupPresentation.expand &&
|
|
933
|
+
expandedGroupKey == item.key
|
|
934
|
+
)
|
|
935
|
+
)
|
|
766
936
|
ToolbarItemKind.separator -> Pair(false, false)
|
|
767
937
|
}
|
|
768
938
|
}
|