react-native-platform-components 0.5.4 → 0.6.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.
- package/README.md +296 -84
- package/android/src/main/java/com/platformcomponents/PCContextMenuView.kt +419 -0
- package/android/src/main/java/com/platformcomponents/PCContextMenuViewManager.kt +200 -0
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +1 -0
- package/ios/PCContextMenu.h +12 -0
- package/ios/PCContextMenu.mm +247 -0
- package/ios/PCContextMenu.swift +346 -0
- package/ios/PCDatePickerView.swift +39 -0
- package/lib/module/ContextMenu.js +111 -0
- package/lib/module/ContextMenu.js.map +1 -0
- package/lib/module/ContextMenuNativeComponent.ts +141 -0
- package/lib/module/SelectionMenu.js +6 -6
- package/lib/module/SelectionMenu.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/ContextMenu.d.ts +79 -0
- package/lib/typescript/src/ContextMenu.d.ts.map +1 -0
- package/lib/typescript/src/ContextMenuNativeComponent.d.ts +122 -0
- package/lib/typescript/src/ContextMenuNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenu.d.ts +6 -5
- package/lib/typescript/src/SelectionMenu.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/sharedTypes.d.ts +3 -1
- package/lib/typescript/src/sharedTypes.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/ContextMenu.tsx +209 -0
- package/src/ContextMenuNativeComponent.ts +141 -0
- package/src/SelectionMenu.tsx +13 -12
- package/src/index.tsx +1 -0
- package/src/sharedTypes.ts +4 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.drawable.Drawable
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.view.Menu
|
|
9
|
+
import android.view.MotionEvent
|
|
10
|
+
import android.view.View
|
|
11
|
+
import android.view.ViewConfiguration
|
|
12
|
+
import android.widget.FrameLayout
|
|
13
|
+
import androidx.appcompat.widget.PopupMenu
|
|
14
|
+
import androidx.core.content.ContextCompat
|
|
15
|
+
import androidx.core.graphics.drawable.DrawableCompat
|
|
16
|
+
|
|
17
|
+
class PCContextMenuView(context: Context) : FrameLayout(context) {
|
|
18
|
+
|
|
19
|
+
data class Action(
|
|
20
|
+
val id: String,
|
|
21
|
+
val title: String,
|
|
22
|
+
val subtitle: String?,
|
|
23
|
+
val image: String?,
|
|
24
|
+
val imageColor: String?,
|
|
25
|
+
val destructive: Boolean,
|
|
26
|
+
val disabled: Boolean,
|
|
27
|
+
val hidden: Boolean,
|
|
28
|
+
val state: String?, // "off" | "on" | "mixed"
|
|
29
|
+
val subactions: List<Action>
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
companion object {
|
|
33
|
+
private const val TAG = "PCContextMenu"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Props ---
|
|
37
|
+
var menuTitle: String? = null
|
|
38
|
+
var actions: List<Action> = emptyList()
|
|
39
|
+
var interactivity: String = "enabled" // "enabled" | "disabled"
|
|
40
|
+
var trigger: String = "longPress" // "longPress" | "tap"
|
|
41
|
+
var androidVisible: String = "closed" // "open" | "closed" (Android-only programmatic)
|
|
42
|
+
var androidAnchorPosition: String? = "left" // "left" | "right"
|
|
43
|
+
|
|
44
|
+
// --- Events ---
|
|
45
|
+
var onPressAction: ((id: String, title: String) -> Unit)? = null
|
|
46
|
+
var onMenuOpen: (() -> Unit)? = null
|
|
47
|
+
var onMenuClose: (() -> Unit)? = null
|
|
48
|
+
|
|
49
|
+
// --- Internal ---
|
|
50
|
+
private var popupMenu: PopupMenu? = null
|
|
51
|
+
private var popupShowing = false
|
|
52
|
+
private var dismissProgrammatic = false
|
|
53
|
+
private var dismissAfterSelect = false
|
|
54
|
+
private var openToken = 0
|
|
55
|
+
|
|
56
|
+
// Long-press detection
|
|
57
|
+
private var longPressRunnable: Runnable? = null
|
|
58
|
+
private var touchDownX = 0f
|
|
59
|
+
private var touchDownY = 0f
|
|
60
|
+
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
61
|
+
private val longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong()
|
|
62
|
+
|
|
63
|
+
init {
|
|
64
|
+
// Enable long-click for longPress mode
|
|
65
|
+
isLongClickable = true
|
|
66
|
+
setOnLongClickListener { handleLongClick() }
|
|
67
|
+
// Enable click for tap mode
|
|
68
|
+
setOnClickListener { handleTap() }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
72
|
+
if (interactivity != "enabled") {
|
|
73
|
+
return super.onInterceptTouchEvent(ev)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
when (ev.action) {
|
|
77
|
+
MotionEvent.ACTION_DOWN -> {
|
|
78
|
+
touchDownX = ev.x
|
|
79
|
+
touchDownY = ev.y
|
|
80
|
+
// In longPress mode, schedule long-press detection
|
|
81
|
+
if (trigger == "longPress") {
|
|
82
|
+
longPressRunnable = Runnable {
|
|
83
|
+
if (handleLongClick()) {
|
|
84
|
+
// Cancel any pending touch events on children
|
|
85
|
+
val cancel = MotionEvent.obtain(
|
|
86
|
+
ev.downTime, System.currentTimeMillis(),
|
|
87
|
+
MotionEvent.ACTION_CANCEL, ev.x, ev.y, 0
|
|
88
|
+
)
|
|
89
|
+
super.dispatchTouchEvent(cancel)
|
|
90
|
+
cancel.recycle()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
postDelayed(longPressRunnable, longPressTimeout)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
MotionEvent.ACTION_MOVE -> {
|
|
97
|
+
// Cancel if moved too much
|
|
98
|
+
if (Math.abs(ev.x - touchDownX) > touchSlop ||
|
|
99
|
+
Math.abs(ev.y - touchDownY) > touchSlop) {
|
|
100
|
+
cancelPendingLongPress()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
MotionEvent.ACTION_UP -> {
|
|
104
|
+
cancelPendingLongPress()
|
|
105
|
+
// In tap mode, handle tap on ACTION_UP if within slop
|
|
106
|
+
if (trigger == "tap" &&
|
|
107
|
+
Math.abs(ev.x - touchDownX) <= touchSlop &&
|
|
108
|
+
Math.abs(ev.y - touchDownY) <= touchSlop) {
|
|
109
|
+
handleTap()
|
|
110
|
+
return true // Consume the event
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
114
|
+
cancelPendingLongPress()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return super.onInterceptTouchEvent(ev)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun cancelPendingLongPress() {
|
|
122
|
+
longPressRunnable?.let {
|
|
123
|
+
removeCallbacks(it)
|
|
124
|
+
longPressRunnable = null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun handleLongClick(): Boolean {
|
|
129
|
+
if (trigger != "longPress") return false
|
|
130
|
+
if (interactivity != "enabled") return false
|
|
131
|
+
|
|
132
|
+
showPopupMenu()
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private fun handleTap() {
|
|
137
|
+
if (trigger != "tap") return
|
|
138
|
+
if (interactivity != "enabled") return
|
|
139
|
+
|
|
140
|
+
showPopupMenu()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- Public apply* (called by manager) ----
|
|
144
|
+
|
|
145
|
+
fun applyMenuTitle(value: String?) {
|
|
146
|
+
menuTitle = value
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fun applyActions(newActions: List<Action>) {
|
|
150
|
+
actions = newActions
|
|
151
|
+
Log.d(TAG, "applyActions size=${actions.size}")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fun applyInteractivity(value: String?) {
|
|
155
|
+
interactivity = if (value == "disabled") "disabled" else "enabled"
|
|
156
|
+
Log.d(TAG, "applyInteractivity interactivity=$interactivity")
|
|
157
|
+
updateEnabledState()
|
|
158
|
+
|
|
159
|
+
// If disabled while menu is open, dismiss
|
|
160
|
+
if (interactivity != "enabled" && popupShowing) {
|
|
161
|
+
Log.d(TAG, "applyInteractivity disabled while open -> dismiss")
|
|
162
|
+
dismissProgrammatic = true
|
|
163
|
+
popupMenu?.dismiss()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fun applyTrigger(value: String?) {
|
|
168
|
+
trigger = when (value) {
|
|
169
|
+
"longPress", "tap" -> value
|
|
170
|
+
else -> "longPress"
|
|
171
|
+
}
|
|
172
|
+
Log.d(TAG, "applyTrigger trigger=$trigger")
|
|
173
|
+
updateTriggerState()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun applyAndroidVisible(value: String?) {
|
|
177
|
+
androidVisible = when (value) {
|
|
178
|
+
"open", "closed" -> value
|
|
179
|
+
else -> "closed"
|
|
180
|
+
}
|
|
181
|
+
Log.d(TAG, "applyAndroidVisible androidVisible=$androidVisible")
|
|
182
|
+
openToken += 1
|
|
183
|
+
val token = openToken
|
|
184
|
+
|
|
185
|
+
if (androidVisible == "open") {
|
|
186
|
+
presentIfNeeded(token)
|
|
187
|
+
} else {
|
|
188
|
+
if (popupShowing) {
|
|
189
|
+
dismissProgrammatic = true
|
|
190
|
+
popupMenu?.dismiss()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fun applyAndroidAnchorPosition(value: String?) {
|
|
196
|
+
androidAnchorPosition = value ?: "left"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---- Internal ----
|
|
200
|
+
|
|
201
|
+
private fun updateEnabledState() {
|
|
202
|
+
val enabled = interactivity == "enabled"
|
|
203
|
+
alpha = if (enabled) 1f else 0.5f
|
|
204
|
+
isLongClickable = enabled && trigger == "longPress"
|
|
205
|
+
isClickable = enabled && trigger == "tap"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private fun updateTriggerState() {
|
|
209
|
+
val enabled = interactivity == "enabled"
|
|
210
|
+
isLongClickable = enabled && trigger == "longPress"
|
|
211
|
+
isClickable = enabled && trigger == "tap"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private fun presentIfNeeded(token: Int) {
|
|
215
|
+
post {
|
|
216
|
+
if (token != openToken) {
|
|
217
|
+
Log.d(TAG, "presentIfNeeded stale token -> skip")
|
|
218
|
+
return@post
|
|
219
|
+
}
|
|
220
|
+
if (androidVisible != "open") {
|
|
221
|
+
Log.d(TAG, "presentIfNeeded no longer open -> skip")
|
|
222
|
+
return@post
|
|
223
|
+
}
|
|
224
|
+
if (interactivity != "enabled") {
|
|
225
|
+
Log.d(TAG, "presentIfNeeded disabled -> skip")
|
|
226
|
+
return@post
|
|
227
|
+
}
|
|
228
|
+
if (!isAttachedToWindow) {
|
|
229
|
+
Log.d(TAG, "presentIfNeeded not attached -> skip")
|
|
230
|
+
return@post
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
showPopupMenu()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@SuppressLint("RestrictedApi")
|
|
238
|
+
private fun showPopupMenu() {
|
|
239
|
+
val visibleActions = actions.filter { !it.hidden }
|
|
240
|
+
if (visibleActions.isEmpty()) {
|
|
241
|
+
Log.d(TAG, "showPopupMenu: no visible actions")
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (popupShowing) {
|
|
246
|
+
Log.d(TAG, "showPopupMenu: already showing")
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
Log.d(TAG, "showPopupMenu: creating popup with ${visibleActions.size} actions")
|
|
251
|
+
|
|
252
|
+
val popup = PopupMenu(context, this)
|
|
253
|
+
|
|
254
|
+
// Try to enable icons in the popup menu
|
|
255
|
+
try {
|
|
256
|
+
val menuHelper = PopupMenu::class.java.getDeclaredField("mPopup")
|
|
257
|
+
menuHelper.isAccessible = true
|
|
258
|
+
val menuPopupHelper = menuHelper.get(popup)
|
|
259
|
+
val setForceShowIcon = menuPopupHelper.javaClass.getDeclaredMethod(
|
|
260
|
+
"setForceShowIcon",
|
|
261
|
+
Boolean::class.java
|
|
262
|
+
)
|
|
263
|
+
setForceShowIcon.invoke(menuPopupHelper, true)
|
|
264
|
+
} catch (e: Exception) {
|
|
265
|
+
Log.d(TAG, "Could not enable popup menu icons: ${e.message}")
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
buildMenu(popup.menu, visibleActions, 0)
|
|
269
|
+
|
|
270
|
+
popup.setOnMenuItemClickListener { item ->
|
|
271
|
+
val actionId = item.intent?.getStringExtra("actionId") ?: ""
|
|
272
|
+
val actionTitle = item.title?.toString() ?: ""
|
|
273
|
+
Log.d(TAG, "popup onMenuItemClick id=$actionId title=$actionTitle")
|
|
274
|
+
dismissAfterSelect = true
|
|
275
|
+
onPressAction?.invoke(actionId, actionTitle)
|
|
276
|
+
true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
popup.setOnDismissListener {
|
|
280
|
+
val programmatic = dismissProgrammatic || dismissAfterSelect
|
|
281
|
+
dismissProgrammatic = false
|
|
282
|
+
dismissAfterSelect = false
|
|
283
|
+
popupShowing = false
|
|
284
|
+
popupMenu = null
|
|
285
|
+
|
|
286
|
+
if (!programmatic) {
|
|
287
|
+
Log.d(TAG, "popup onDismiss user-initiated")
|
|
288
|
+
}
|
|
289
|
+
onMenuClose?.invoke()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
popupMenu = popup
|
|
293
|
+
popupShowing = true
|
|
294
|
+
dismissProgrammatic = false
|
|
295
|
+
dismissAfterSelect = false
|
|
296
|
+
|
|
297
|
+
onMenuOpen?.invoke()
|
|
298
|
+
popup.show()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private fun buildMenu(menu: Menu, actions: List<Action>, groupId: Int) {
|
|
302
|
+
var order = 0
|
|
303
|
+
for (action in actions) {
|
|
304
|
+
if (action.hidden) continue
|
|
305
|
+
|
|
306
|
+
if (action.subactions.isNotEmpty()) {
|
|
307
|
+
// Create submenu
|
|
308
|
+
val subMenu = menu.addSubMenu(groupId, order, order, action.title)
|
|
309
|
+
buildMenu(subMenu, action.subactions, groupId + 1)
|
|
310
|
+
|
|
311
|
+
// Set icon on submenu header if available
|
|
312
|
+
action.image?.let { imageName ->
|
|
313
|
+
getDrawableByName(imageName)?.let { drawable ->
|
|
314
|
+
val tintedDrawable = tintDrawable(drawable, action.imageColor)
|
|
315
|
+
subMenu.item.icon = tintedDrawable
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
// Create regular item
|
|
320
|
+
val item = menu.add(groupId, order, order, buildTitle(action))
|
|
321
|
+
|
|
322
|
+
// Store action ID in intent for retrieval
|
|
323
|
+
item.intent = android.content.Intent().apply {
|
|
324
|
+
putExtra("actionId", action.id)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Set enabled state
|
|
328
|
+
item.isEnabled = !action.disabled
|
|
329
|
+
|
|
330
|
+
// Set icon
|
|
331
|
+
action.image?.let { imageName ->
|
|
332
|
+
getDrawableByName(imageName)?.let { drawable ->
|
|
333
|
+
val tintedDrawable = tintDrawable(drawable, action.imageColor)
|
|
334
|
+
item.icon = tintedDrawable
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Set checkable state
|
|
339
|
+
when (action.state) {
|
|
340
|
+
"on" -> {
|
|
341
|
+
item.isCheckable = true
|
|
342
|
+
item.isChecked = true
|
|
343
|
+
}
|
|
344
|
+
"mixed" -> {
|
|
345
|
+
item.isCheckable = true
|
|
346
|
+
item.isChecked = true // Android doesn't have "mixed", treat as checked
|
|
347
|
+
}
|
|
348
|
+
else -> {
|
|
349
|
+
item.isCheckable = false
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
order++
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private fun buildTitle(action: Action): String {
|
|
358
|
+
// On Android, we can't easily style text as destructive in PopupMenu
|
|
359
|
+
// We could prefix with emoji or special character, but keeping it simple
|
|
360
|
+
return action.title
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private fun getDrawableByName(name: String): Drawable? {
|
|
364
|
+
// First try as a drawable resource
|
|
365
|
+
val resourceId = context.resources.getIdentifier(
|
|
366
|
+
name,
|
|
367
|
+
"drawable",
|
|
368
|
+
context.packageName
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if (resourceId != 0) {
|
|
372
|
+
return ContextCompat.getDrawable(context, resourceId)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Try common Material icons (ic_ prefix)
|
|
376
|
+
val icResourceId = context.resources.getIdentifier(
|
|
377
|
+
"ic_$name",
|
|
378
|
+
"drawable",
|
|
379
|
+
context.packageName
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if (icResourceId != 0) {
|
|
383
|
+
return ContextCompat.getDrawable(context, icResourceId)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
Log.d(TAG, "Drawable not found: $name")
|
|
387
|
+
return null
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private fun tintDrawable(drawable: Drawable, colorString: String?): Drawable {
|
|
391
|
+
if (colorString.isNullOrEmpty()) return drawable
|
|
392
|
+
|
|
393
|
+
val color = parseColor(colorString) ?: return drawable
|
|
394
|
+
|
|
395
|
+
val wrappedDrawable = DrawableCompat.wrap(drawable.mutate())
|
|
396
|
+
DrawableCompat.setTint(wrappedDrawable, color)
|
|
397
|
+
return wrappedDrawable
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private fun parseColor(colorString: String): Int? {
|
|
401
|
+
return try {
|
|
402
|
+
// Support hex colors like "#FF0000" or "FF0000"
|
|
403
|
+
val hex = if (colorString.startsWith("#")) colorString else "#$colorString"
|
|
404
|
+
Color.parseColor(hex)
|
|
405
|
+
} catch (e: Exception) {
|
|
406
|
+
Log.d(TAG, "Failed to parse color: $colorString")
|
|
407
|
+
null
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
override fun onDetachedFromWindow() {
|
|
412
|
+
cancelPendingLongPress()
|
|
413
|
+
if (popupShowing) {
|
|
414
|
+
dismissProgrammatic = true
|
|
415
|
+
popupMenu?.dismiss()
|
|
416
|
+
}
|
|
417
|
+
super.onDetachedFromWindow()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import com.facebook.react.bridge.ReadableArray
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
8
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
9
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
10
|
+
import com.facebook.react.uimanager.events.Event
|
|
11
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
12
|
+
import com.facebook.react.viewmanagers.PCContextMenuManagerDelegate
|
|
13
|
+
import com.facebook.react.viewmanagers.PCContextMenuManagerInterface
|
|
14
|
+
|
|
15
|
+
class PCContextMenuViewManager :
|
|
16
|
+
ViewGroupManager<PCContextMenuView>(),
|
|
17
|
+
PCContextMenuManagerInterface<PCContextMenuView> {
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
private const val TAG = "PCContextMenu"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private val delegate: ViewManagerDelegate<PCContextMenuView> =
|
|
24
|
+
PCContextMenuManagerDelegate(this)
|
|
25
|
+
|
|
26
|
+
override fun getName(): String = "PCContextMenu"
|
|
27
|
+
|
|
28
|
+
override fun getDelegate(): ViewManagerDelegate<PCContextMenuView> = delegate
|
|
29
|
+
|
|
30
|
+
override fun createViewInstance(reactContext: ThemedReactContext): PCContextMenuView {
|
|
31
|
+
return PCContextMenuView(reactContext)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override fun addEventEmitters(reactContext: ThemedReactContext, view: PCContextMenuView) {
|
|
35
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
|
|
36
|
+
|
|
37
|
+
view.onPressAction = { id, title ->
|
|
38
|
+
dispatcher?.dispatchEvent(PressActionEvent(view.id, id, title))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
view.onMenuOpen = {
|
|
42
|
+
dispatcher?.dispatchEvent(MenuOpenEvent(view.id))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
view.onMenuClose = {
|
|
46
|
+
dispatcher?.dispatchEvent(MenuCloseEvent(view.id))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun setTitle(view: PCContextMenuView, value: String?) {
|
|
51
|
+
view.applyMenuTitle(value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun setActions(view: PCContextMenuView, value: ReadableArray?) {
|
|
55
|
+
val out = ArrayList<PCContextMenuView.Action>()
|
|
56
|
+
if (value != null) {
|
|
57
|
+
for (i in 0 until value.size()) {
|
|
58
|
+
val m = value.getMap(i) ?: continue
|
|
59
|
+
out.add(parseAction(m))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
view.applyActions(out)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private fun parseAction(map: ReadableMap): PCContextMenuView.Action {
|
|
66
|
+
val id = map.getStringOrEmpty("id")
|
|
67
|
+
val title = map.getStringOrEmpty("title")
|
|
68
|
+
val subtitle = map.getStringOrNull("subtitle")
|
|
69
|
+
val image = map.getStringOrNull("image")
|
|
70
|
+
val imageColor = map.getStringOrNull("imageColor")
|
|
71
|
+
val state = map.getStringOrNull("state")
|
|
72
|
+
|
|
73
|
+
// Parse attributes
|
|
74
|
+
var destructive = false
|
|
75
|
+
var disabled = false
|
|
76
|
+
var hidden = false
|
|
77
|
+
if (map.hasKey("attributes") && !map.isNull("attributes")) {
|
|
78
|
+
val attrs = map.getMap("attributes")
|
|
79
|
+
if (attrs != null) {
|
|
80
|
+
destructive = attrs.getStringOrEmpty("destructive") == "true"
|
|
81
|
+
disabled = attrs.getStringOrEmpty("disabled") == "true"
|
|
82
|
+
hidden = attrs.getStringOrEmpty("hidden") == "true"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse subactions recursively
|
|
87
|
+
val subactions = ArrayList<PCContextMenuView.Action>()
|
|
88
|
+
if (map.hasKey("subactions") && !map.isNull("subactions")) {
|
|
89
|
+
val subs = map.getArray("subactions")
|
|
90
|
+
if (subs != null) {
|
|
91
|
+
for (j in 0 until subs.size()) {
|
|
92
|
+
val subMap = subs.getMap(j) ?: continue
|
|
93
|
+
subactions.add(parseAction(subMap))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return PCContextMenuView.Action(
|
|
99
|
+
id = id,
|
|
100
|
+
title = title,
|
|
101
|
+
subtitle = subtitle,
|
|
102
|
+
image = image,
|
|
103
|
+
imageColor = imageColor,
|
|
104
|
+
destructive = destructive,
|
|
105
|
+
disabled = disabled,
|
|
106
|
+
hidden = hidden,
|
|
107
|
+
state = state,
|
|
108
|
+
subactions = subactions
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun ReadableMap.getStringOrEmpty(key: String): String {
|
|
113
|
+
return if (hasKey(key) && !isNull(key)) getString(key) ?: "" else ""
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun ReadableMap.getStringOrNull(key: String): String? {
|
|
117
|
+
return if (hasKey(key) && !isNull(key)) getString(key) else null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override fun setInteractivity(view: PCContextMenuView, value: String?) {
|
|
121
|
+
view.applyInteractivity(value)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
override fun setTrigger(view: PCContextMenuView, value: String?) {
|
|
125
|
+
view.applyTrigger(value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override fun setIos(view: PCContextMenuView, value: ReadableMap?) {
|
|
129
|
+
// Android ignores iOS config
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
override fun setAndroid(view: PCContextMenuView, value: ReadableMap?) {
|
|
133
|
+
if (value == null) {
|
|
134
|
+
view.applyAndroidAnchorPosition(null)
|
|
135
|
+
view.applyAndroidVisible(null)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
val anchorPosition = if (value.hasKey("anchorPosition") && !value.isNull("anchorPosition")) {
|
|
140
|
+
value.getString("anchorPosition")
|
|
141
|
+
} else {
|
|
142
|
+
null
|
|
143
|
+
}
|
|
144
|
+
view.applyAndroidAnchorPosition(anchorPosition)
|
|
145
|
+
|
|
146
|
+
val visible = if (value.hasKey("visible") && !value.isNull("visible")) {
|
|
147
|
+
value.getString("visible")
|
|
148
|
+
} else {
|
|
149
|
+
null
|
|
150
|
+
}
|
|
151
|
+
view.applyAndroidVisible(visible)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- ViewGroupManager child management ---
|
|
155
|
+
override fun addView(parent: PCContextMenuView, child: View, index: Int) {
|
|
156
|
+
parent.addView(child, index)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override fun removeViewAt(parent: PCContextMenuView, index: Int) {
|
|
160
|
+
parent.removeViewAt(index)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override fun getChildCount(parent: PCContextMenuView): Int {
|
|
164
|
+
return parent.childCount
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override fun getChildAt(parent: PCContextMenuView, index: Int): View? {
|
|
168
|
+
return parent.getChildAt(index)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Events ---
|
|
172
|
+
private class PressActionEvent(
|
|
173
|
+
surfaceId: Int,
|
|
174
|
+
private val actionId: String,
|
|
175
|
+
private val actionTitle: String
|
|
176
|
+
) : Event<PressActionEvent>(surfaceId) {
|
|
177
|
+
override fun getEventName(): String = "topPressAction"
|
|
178
|
+
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
|
|
179
|
+
val payload = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
180
|
+
putString("actionId", actionId)
|
|
181
|
+
putString("actionTitle", actionTitle)
|
|
182
|
+
}
|
|
183
|
+
rctEventEmitter.receiveEvent(viewTag, eventName, payload)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private class MenuOpenEvent(surfaceId: Int) : Event<MenuOpenEvent>(surfaceId) {
|
|
188
|
+
override fun getEventName(): String = "topMenuOpen"
|
|
189
|
+
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
|
|
190
|
+
rctEventEmitter.receiveEvent(viewTag, eventName, com.facebook.react.bridge.Arguments.createMap())
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private class MenuCloseEvent(surfaceId: Int) : Event<MenuCloseEvent>(surfaceId) {
|
|
195
|
+
override fun getEventName(): String = "topMenuClose"
|
|
196
|
+
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
|
|
197
|
+
rctEventEmitter.receiveEvent(viewTag, eventName, com.facebook.react.bridge.Arguments.createMap())
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|