@thelacanians/vue-native-cli 0.4.13 → 0.4.15
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/dist/cli.js +472 -192
- package/native/android/VueNativeCore/build.gradle.kts +1 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +38 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +33 -13
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +27 -6
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSliderFactory.kt +9 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/EventThrottle.kt +57 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +79 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/JSPolyfills.swift +15 -2
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +106 -112
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +19 -2
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VScrollViewFactory.swift +9 -4
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSliderFactory.swift +8 -3
- package/package.json +1 -1
package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt
CHANGED
|
@@ -23,6 +23,20 @@ class NativeBridge(private val context: Context) {
|
|
|
23
23
|
"create", "createText", "appendChild", "insertBefore", "removeChild",
|
|
24
24
|
"setRootView", "setText", "setElementText"
|
|
25
25
|
)
|
|
26
|
+
|
|
27
|
+
/** Style properties that affect layout and require a layout pass when changed. */
|
|
28
|
+
private val layoutAffectingStyles = setOf(
|
|
29
|
+
"width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
|
|
30
|
+
"flex", "flexGrow", "flexShrink", "flexBasis", "flexDirection",
|
|
31
|
+
"flexWrap", "alignItems", "alignSelf", "alignContent", "justifyContent",
|
|
32
|
+
"padding", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
|
|
33
|
+
"paddingHorizontal", "paddingVertical", "paddingStart", "paddingEnd",
|
|
34
|
+
"margin", "marginTop", "marginRight", "marginBottom", "marginLeft",
|
|
35
|
+
"marginHorizontal", "marginVertical", "marginStart", "marginEnd",
|
|
36
|
+
"gap", "rowGap", "columnGap",
|
|
37
|
+
"position", "top", "right", "bottom", "left", "start", "end",
|
|
38
|
+
"aspectRatio", "display", "overflow", "direction",
|
|
39
|
+
)
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
@@ -81,20 +95,34 @@ class NativeBridge(private val context: Context) {
|
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
mainHandler.post {
|
|
84
|
-
var
|
|
98
|
+
var needsLayout = false
|
|
85
99
|
for (op in ops) {
|
|
86
100
|
try {
|
|
87
101
|
val opName = op.optString("op")
|
|
88
|
-
if (!
|
|
89
|
-
|
|
102
|
+
if (!needsLayout && opName in treeMutationOps) {
|
|
103
|
+
needsLayout = true
|
|
104
|
+
}
|
|
105
|
+
// Check if updateStyle changes any layout-affecting property
|
|
106
|
+
if (!needsLayout && opName == "updateStyle") {
|
|
107
|
+
val args = op.optJSONArray("args")
|
|
108
|
+
val styleObj = args?.optJSONObject(1)
|
|
109
|
+
if (styleObj != null) {
|
|
110
|
+
val keys = styleObj.keys()
|
|
111
|
+
while (keys.hasNext()) {
|
|
112
|
+
if (keys.next() in layoutAffectingStyles) {
|
|
113
|
+
needsLayout = true
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
90
118
|
}
|
|
91
119
|
handleOperation(op)
|
|
92
120
|
} catch (e: Exception) {
|
|
93
121
|
Log.e(TAG, "Error handling op '${op.optString("op")}': ${e.message}")
|
|
94
122
|
}
|
|
95
123
|
}
|
|
96
|
-
//
|
|
97
|
-
if (
|
|
124
|
+
// Trigger layout when tree was mutated or layout-affecting styles changed
|
|
125
|
+
if (needsLayout) {
|
|
98
126
|
triggerLayout()
|
|
99
127
|
}
|
|
100
128
|
}
|
|
@@ -380,6 +408,11 @@ class NativeBridge(private val context: Context) {
|
|
|
380
408
|
|
|
381
409
|
/** Clear all registries. Called during hot reload after JS teardown. Must run on main thread. */
|
|
382
410
|
fun clearAllRegistries() {
|
|
411
|
+
// Destroy all views via their factories before clearing maps
|
|
412
|
+
for ((_, view) in nodeViews) {
|
|
413
|
+
val factory = componentRegistry.factoryForView(view)
|
|
414
|
+
factory?.destroyView(view)
|
|
415
|
+
}
|
|
383
416
|
nodeViews.clear()
|
|
384
417
|
nodeTypes.clear()
|
|
385
418
|
eventHandlers.clear()
|
|
@@ -98,9 +98,16 @@ class VListFactory : NativeComponentFactory {
|
|
|
98
98
|
cumulativeX += dx
|
|
99
99
|
cumulativeY += dy
|
|
100
100
|
|
|
101
|
-
// Dispatch scroll event with cumulative position
|
|
101
|
+
// Dispatch scroll event with cumulative position and dimensions
|
|
102
102
|
scrollHandlers[recyclerView]?.invoke(
|
|
103
|
-
mapOf(
|
|
103
|
+
mapOf(
|
|
104
|
+
"x" to cumulativeX,
|
|
105
|
+
"y" to cumulativeY,
|
|
106
|
+
"contentWidth" to (recyclerView.computeHorizontalScrollRange()),
|
|
107
|
+
"contentHeight" to (recyclerView.computeVerticalScrollRange()),
|
|
108
|
+
"layoutWidth" to recyclerView.width,
|
|
109
|
+
"layoutHeight" to recyclerView.height,
|
|
110
|
+
)
|
|
104
111
|
)
|
|
105
112
|
|
|
106
113
|
// endReached detection (threshold = 20% from bottom)
|
|
@@ -167,20 +174,33 @@ class VListFactory : NativeComponentFactory {
|
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
class VListAdapter(private val items: List<View>) : RecyclerView.Adapter<VListAdapter.VH>() {
|
|
170
|
-
class VH(
|
|
177
|
+
class VH(itemView: View) : RecyclerView.ViewHolder(itemView)
|
|
171
178
|
|
|
172
179
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
|
173
|
-
val
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
180
|
-
|
|
180
|
+
val container = android.widget.FrameLayout(parent.context).apply {
|
|
181
|
+
layoutParams = RecyclerView.LayoutParams(
|
|
182
|
+
RecyclerView.LayoutParams.MATCH_PARENT,
|
|
183
|
+
RecyclerView.LayoutParams.WRAP_CONTENT
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
return VH(container)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
override fun onBindViewHolder(holder: VH, position: Int) {
|
|
190
|
+
val container = holder.itemView as android.widget.FrameLayout
|
|
191
|
+
// Remove the previous item view from this recycled container
|
|
192
|
+
container.removeAllViews()
|
|
193
|
+
val itemView = items.getOrNull(position) ?: return
|
|
194
|
+
// Remove from any existing parent (previous container or direct parent)
|
|
195
|
+
(itemView.parent as? ViewGroup)?.removeView(itemView)
|
|
196
|
+
container.addView(itemView, ViewGroup.LayoutParams(
|
|
197
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
198
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
199
|
+
))
|
|
181
200
|
}
|
|
182
201
|
|
|
183
|
-
override fun onBindViewHolder(holder: VH, position: Int) {}
|
|
184
202
|
override fun getItemCount(): Int = items.size
|
|
185
|
-
|
|
203
|
+
|
|
204
|
+
// Use a single view type so RecyclerView can reuse all ViewHolders
|
|
205
|
+
override fun getItemViewType(position: Int): Int = 0
|
|
186
206
|
}
|
|
@@ -64,6 +64,9 @@ class VScrollViewFactory : NativeComponentFactory {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
private val scrollThrottles = mutableMapOf<SwipeRefreshLayout, EventThrottle>()
|
|
68
|
+
private val scrollListeners = mutableMapOf<SwipeRefreshLayout, android.view.ViewTreeObserver.OnScrollChangedListener>()
|
|
69
|
+
|
|
67
70
|
override fun addEventListener(view: View, event: String, handler: (Any?) -> Unit) {
|
|
68
71
|
val srf = view as? SwipeRefreshLayout ?: return
|
|
69
72
|
val scroll = states[srf]?.scrollView
|
|
@@ -73,20 +76,38 @@ class VScrollViewFactory : NativeComponentFactory {
|
|
|
73
76
|
srf.setOnRefreshListener { handler(null) }
|
|
74
77
|
}
|
|
75
78
|
"scroll" -> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
val throttle = EventThrottle(intervalMs = 16L, handler = handler)
|
|
80
|
+
scrollThrottles[srf] = throttle
|
|
81
|
+
val listener = android.view.ViewTreeObserver.OnScrollChangedListener {
|
|
82
|
+
val child = if (scroll != null && scroll.childCount > 0) scroll.getChildAt(0) else null
|
|
83
|
+
throttle.fire(mapOf(
|
|
84
|
+
"x" to (scroll?.scrollX ?: 0),
|
|
85
|
+
"y" to (scroll?.scrollY ?: 0),
|
|
86
|
+
"contentWidth" to (child?.width ?: scroll?.width ?: 0),
|
|
87
|
+
"contentHeight" to (child?.height ?: scroll?.height ?: 0),
|
|
88
|
+
"layoutWidth" to (scroll?.width ?: 0),
|
|
89
|
+
"layoutHeight" to (scroll?.height ?: 0),
|
|
79
90
|
))
|
|
80
91
|
}
|
|
92
|
+
scrollListeners[srf] = listener
|
|
93
|
+
scroll?.viewTreeObserver?.addOnScrollChangedListener(listener)
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
override fun removeEventListener(view: View, event: String) {
|
|
86
99
|
val srf = view as? SwipeRefreshLayout ?: return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
when (event) {
|
|
101
|
+
"refresh" -> {
|
|
102
|
+
srf.setOnRefreshListener(null)
|
|
103
|
+
srf.isEnabled = false
|
|
104
|
+
}
|
|
105
|
+
"scroll" -> {
|
|
106
|
+
scrollListeners.remove(srf)?.let { listener ->
|
|
107
|
+
states[srf]?.scrollView?.viewTreeObserver?.removeOnScrollChangedListener(listener)
|
|
108
|
+
}
|
|
109
|
+
scrollThrottles.remove(srf)?.cancel()
|
|
110
|
+
}
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
113
|
|
|
@@ -48,18 +48,24 @@ class VSliderFactory : NativeComponentFactory {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
private val throttles = mutableMapOf<SeekBar, EventThrottle>()
|
|
52
|
+
|
|
51
53
|
override fun addEventListener(view: View, event: String, handler: (Any?) -> Unit) {
|
|
52
54
|
val sb = view as? SeekBar ?: return
|
|
53
55
|
when (event) {
|
|
54
56
|
"change", "valueChange" -> {
|
|
55
57
|
changeHandlers[sb] = handler
|
|
58
|
+
val throttle = EventThrottle(intervalMs = 16L, handler = handler)
|
|
59
|
+
throttles[sb] = throttle
|
|
56
60
|
sb.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
57
61
|
override fun onProgressChanged(s: SeekBar, progress: Int, fromUser: Boolean) {
|
|
58
|
-
if (fromUser)
|
|
62
|
+
if (fromUser) throttle.fire(mapOf("value" to progress.toFloat() / s.max.toFloat()))
|
|
59
63
|
}
|
|
60
64
|
override fun onStartTrackingTouch(s: SeekBar) {}
|
|
61
65
|
override fun onStopTrackingTouch(s: SeekBar) {
|
|
62
|
-
|
|
66
|
+
// Always deliver the final value immediately on release
|
|
67
|
+
throttle.cancel()
|
|
68
|
+
changeHandlers[s]?.invoke(mapOf("value" to s.progress.toFloat() / s.max.toFloat()))
|
|
63
69
|
}
|
|
64
70
|
})
|
|
65
71
|
}
|
|
@@ -69,6 +75,7 @@ class VSliderFactory : NativeComponentFactory {
|
|
|
69
75
|
override fun removeEventListener(view: View, event: String) {
|
|
70
76
|
val sb = view as? SeekBar ?: return
|
|
71
77
|
changeHandlers.remove(sb)
|
|
78
|
+
throttles.remove(sb)?.cancel()
|
|
72
79
|
sb.setOnSeekBarChangeListener(null)
|
|
73
80
|
}
|
|
74
81
|
}
|
package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/EventThrottle.kt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package com.vuenative.core
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.os.SystemClock
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Throttles high-frequency event handlers to avoid flooding the JS bridge.
|
|
9
|
+
*
|
|
10
|
+
* When a high-frequency event (scroll, slider drag) fires many times per frame,
|
|
11
|
+
* each invocation becomes a bridge round-trip. This utility ensures at most one
|
|
12
|
+
* call per [intervalMs] milliseconds, with a trailing call to deliver the latest value.
|
|
13
|
+
*
|
|
14
|
+
* Default interval: 16ms (~60 FPS).
|
|
15
|
+
*/
|
|
16
|
+
class EventThrottle(
|
|
17
|
+
private val intervalMs: Long = 16L,
|
|
18
|
+
private val handler: (Any?) -> Unit
|
|
19
|
+
) {
|
|
20
|
+
private var lastFireTime: Long = 0
|
|
21
|
+
private var pendingTrailing = false
|
|
22
|
+
private var latestPayload: Any? = null
|
|
23
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
24
|
+
|
|
25
|
+
private val trailingRunnable = Runnable {
|
|
26
|
+
lastFireTime = SystemClock.uptimeMillis()
|
|
27
|
+
pendingTrailing = false
|
|
28
|
+
handler(latestPayload)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Call this from the native event callback instead of the original handler.
|
|
33
|
+
* Fires immediately if enough time has elapsed, otherwise schedules a trailing call.
|
|
34
|
+
*/
|
|
35
|
+
fun fire(payload: Any?) {
|
|
36
|
+
val now = SystemClock.uptimeMillis()
|
|
37
|
+
val elapsed = now - lastFireTime
|
|
38
|
+
latestPayload = payload
|
|
39
|
+
|
|
40
|
+
if (elapsed >= intervalMs) {
|
|
41
|
+
lastFireTime = now
|
|
42
|
+
pendingTrailing = false
|
|
43
|
+
mainHandler.removeCallbacks(trailingRunnable)
|
|
44
|
+
handler(payload)
|
|
45
|
+
} else if (!pendingTrailing) {
|
|
46
|
+
pendingTrailing = true
|
|
47
|
+
mainHandler.postDelayed(trailingRunnable, intervalMs - elapsed)
|
|
48
|
+
}
|
|
49
|
+
// If trailing is already pending, latestPayload is updated above
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Cancel any pending trailing call. */
|
|
53
|
+
fun cancel() {
|
|
54
|
+
mainHandler.removeCallbacks(trailingRunnable)
|
|
55
|
+
pendingTrailing = false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Throttles high-frequency event handlers to avoid flooding the JS bridge.
|
|
5
|
+
///
|
|
6
|
+
/// When a high-frequency event (scroll, slider drag, text input) fires many
|
|
7
|
+
/// times per frame, each invocation becomes a bridge round-trip. This utility
|
|
8
|
+
/// ensures at most one call per `interval` seconds, with a trailing call
|
|
9
|
+
/// to deliver the latest value.
|
|
10
|
+
///
|
|
11
|
+
/// Default interval: 16ms (~60 FPS). Callers can customize via `interval`.
|
|
12
|
+
final class EventThrottle {
|
|
13
|
+
|
|
14
|
+
/// Minimum time between handler invocations (seconds).
|
|
15
|
+
let interval: TimeInterval
|
|
16
|
+
|
|
17
|
+
/// The throttled handler to call.
|
|
18
|
+
private let handler: (Any?) -> Void
|
|
19
|
+
|
|
20
|
+
/// Timestamp of the last invocation.
|
|
21
|
+
private var lastFireTime: CFTimeInterval = 0
|
|
22
|
+
|
|
23
|
+
/// Whether a trailing call is pending.
|
|
24
|
+
private var pendingTrailing = false
|
|
25
|
+
|
|
26
|
+
/// The most recent payload, used for the trailing call.
|
|
27
|
+
private var latestPayload: Any?
|
|
28
|
+
|
|
29
|
+
/// Timer for delivering the trailing call.
|
|
30
|
+
private var trailingTimer: DispatchSourceTimer?
|
|
31
|
+
|
|
32
|
+
/// Create a throttled event handler.
|
|
33
|
+
/// - Parameters:
|
|
34
|
+
/// - interval: Minimum seconds between invocations. Default 0.016 (~60fps).
|
|
35
|
+
/// - handler: The closure to invoke with the event payload.
|
|
36
|
+
init(interval: TimeInterval = 0.016, handler: @escaping (Any?) -> Void) {
|
|
37
|
+
self.interval = interval
|
|
38
|
+
self.handler = handler
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deinit {
|
|
42
|
+
trailingTimer?.cancel()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Call this from the native event callback instead of the original handler.
|
|
46
|
+
/// Fires immediately if enough time has elapsed, otherwise schedules a trailing call.
|
|
47
|
+
func fire(_ payload: Any?) {
|
|
48
|
+
let now = CACurrentMediaTime()
|
|
49
|
+
let elapsed = now - lastFireTime
|
|
50
|
+
|
|
51
|
+
latestPayload = payload
|
|
52
|
+
|
|
53
|
+
if elapsed >= interval {
|
|
54
|
+
// Enough time has passed — fire immediately
|
|
55
|
+
lastFireTime = now
|
|
56
|
+
pendingTrailing = false
|
|
57
|
+
trailingTimer?.cancel()
|
|
58
|
+
trailingTimer = nil
|
|
59
|
+
handler(payload)
|
|
60
|
+
} else if !pendingTrailing {
|
|
61
|
+
// Schedule a trailing call for the remaining time
|
|
62
|
+
pendingTrailing = true
|
|
63
|
+
let remaining = interval - elapsed
|
|
64
|
+
let timer = DispatchSource.makeTimerSource(queue: .main)
|
|
65
|
+
timer.schedule(deadline: .now() + remaining)
|
|
66
|
+
timer.setEventHandler { [weak self] in
|
|
67
|
+
guard let self = self else { return }
|
|
68
|
+
self.lastFireTime = CACurrentMediaTime()
|
|
69
|
+
self.pendingTrailing = false
|
|
70
|
+
self.trailingTimer = nil
|
|
71
|
+
self.handler(self.latestPayload)
|
|
72
|
+
}
|
|
73
|
+
trailingTimer = timer
|
|
74
|
+
timer.resume()
|
|
75
|
+
}
|
|
76
|
+
// If a trailing call is already pending, just update latestPayload (done above)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
#endif
|
|
@@ -704,8 +704,21 @@ enum JSPolyfills {
|
|
|
704
704
|
/// Produce a JSON-safe string literal (with quotes) for embedding in JS eval strings.
|
|
705
705
|
private enum JSPolyfillsJSON {
|
|
706
706
|
static func encode(_ str: String) -> String {
|
|
707
|
-
|
|
708
|
-
|
|
707
|
+
// Wrap in array so JSONSerialization gets a valid top-level type.
|
|
708
|
+
// String alone causes an NSException that try? cannot catch.
|
|
709
|
+
if let data = try? JSONSerialization.data(withJSONObject: [str]),
|
|
710
|
+
let json = String(data: data, encoding: .utf8),
|
|
711
|
+
json.count >= 2 {
|
|
712
|
+
return String(json.dropFirst().dropLast())
|
|
713
|
+
}
|
|
714
|
+
// Fallback: manual escaping
|
|
715
|
+
let escaped = str
|
|
716
|
+
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
717
|
+
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
718
|
+
.replacingOccurrences(of: "\n", with: "\\n")
|
|
719
|
+
.replacingOccurrences(of: "\r", with: "\\r")
|
|
720
|
+
.replacingOccurrences(of: "\t", with: "\\t")
|
|
721
|
+
return "\"\(escaped)\""
|
|
709
722
|
}
|
|
710
723
|
}
|
|
711
724
|
|