Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.tencent.kuikly.core.render.web.collection.map.JsMap
import com.tencent.kuikly.core.render.web.collection.map.get
import com.tencent.kuikly.core.render.web.collection.map.remove
import com.tencent.kuikly.core.render.web.collection.map.set
import com.tencent.kuikly.core.render.web.const.KRExtraConst
import com.tencent.kuikly.core.render.web.context.KuiklyRenderCoreExecuteMode
import com.tencent.kuikly.core.render.web.core.IKuiklyRenderContextInitCallback
import com.tencent.kuikly.core.render.web.core.IKuiklyRenderCore
Expand Down Expand Up @@ -247,6 +248,8 @@ class KuiklyRenderView(
put(ACTIVITY_HEIGHT, params[ACTIVITY_HEIGHT] ?: 0)
put(SAFE_AREA_INSETS, "$statusBarHeight 0 0 0")
put(APP_VERSION, kuiklyWindow.navigator.appVersion)
// Native build >= 2 will use modalView instead of view when isWindow is true
put(NATIVE_BUILD, params[NATIVE_BUILD] ?: 0)

// Page parameters
put(PARAMS, params[PARAMS] ?: mutableMapOf<String, Any>())
Expand Down Expand Up @@ -288,6 +291,8 @@ class KuiklyRenderView(
}
}
)
// set container element id
setContainerElementId(getInstanceId())
}
dispatchLifecycleStateChanged(STATE_INIT_CORE_FINISH)
}
Expand Down Expand Up @@ -389,6 +394,14 @@ class KuiklyRenderView(
}
}

/**
* Set container element ID with prefix
*/
private fun setContainerElementId(containerElementId: String) {
// Add prefix,eg "kuikly_web_container_0"
this._container.id = "${KRExtraConst.WEB_CONTAINER_PREFIX}${containerElementId}"
}

companion object {
// pageData passed in parameters
private const val ROOT_VIEW_WIDTH = "rootViewWidth"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ object KRCssConst {
const val AUTO_DARK_ENABLE = "autoDarkEnable"
const val TURBO_DISPLAY_AUTO_UPDATE_ENABLE = "turboDisplayAutoUpdateEnable"
const val SCROLL_INDEX = "scrollIndex"

// Frame related attributes
val FRAME_ATTRS = listOf("width", "height", "left", "top")
}
Expand Down Expand Up @@ -374,3 +374,9 @@ object KRPlaceholderConst {
const val CLASS_PREFIX = "phcolor_"
const val MAX_RANDOM = 1_000_000
}

object KRExtraConst {
const val WEB_CONTAINER_PREFIX = "kuikly_web_container_"
const val WEB_DECREASE_CALLKOTLIN_ID_METHOD = "webDecreaseCallKotlinIdMethod"
const val COMPONENT_IDENTIFIER_KEY = "data-kuikly-component"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.tencent.kuikly.core.render.web.collection.array.fourthArg
import com.tencent.kuikly.core.render.web.collection.array.secondArg
import com.tencent.kuikly.core.render.web.collection.array.sixthArg
import com.tencent.kuikly.core.render.web.collection.array.thirdArg
import com.tencent.kuikly.core.render.web.const.KRExtraConst.WEB_DECREASE_CALLKOTLIN_ID_METHOD
import com.tencent.kuikly.core.render.web.ktx.KuiklyRenderNativeMethodCallback
import com.tencent.kuikly.core.render.web.ktx.kuiklyWindow
import com.tencent.kuikly.core.render.web.ktx.toJSONArray
Expand All @@ -28,16 +29,26 @@ class KuiklyRenderContextHandler : IKuiklyRenderContextHandler {
* Initialize context, register methods for kotlin side to call native side
*/
override fun init(url: String?, pageId: String) {
// handle multi-closure registerCallNative before calling it,
// so that the broadcast interception is set up first
handleMultiInstanceRegisterCallNative()
// Register callNative global method for JS environment, for kotlin to call Native
// Using multi-instance callNative registration method from core, passing instanceId
// to core for single page multi-instance distinction
try {
// Web host registers native communication interface for kuikly to call
// Web host registers native communication interface for kuikly to call.
// After handleMultiInstanceRegisterCallNative(), this call triggers the broadcast
// function which dispatches to all known closures' registerCallNative, ensuring
// the correct BridgeManager receives the registration regardless of closure load order.
kuiklyWindow.asDynamic().com.tencent.kuikly.core.nvi.registerCallNative(pageId, ::callNative)
// save pageId map
instanceIdMap[pageId] = instanceId
} catch (e: dynamic) {
// Call error
Log.error("registerCallNative error, reason is: $e")
}
// handle multiple instances
handleMultiInstanceCallKotlinMethod()
}

/**
Expand Down Expand Up @@ -140,9 +151,167 @@ class KuiklyRenderContextHandler : IKuiklyRenderContextHandler {
)
}

/**
* Handle multi-closure registerCallNative problem.
*
* Uses Object.defineProperty to intercept assignments to registerCallNative on the kuikly
* namespace. Each time a new closure loads and assigns its own registerCallNative, the setter
* saves it under a unique key. The getter returns a broadcast function that calls ALL saved
* functions, so the correct closure's BridgeManager always receives the registration.
*
* IMPORTANT: Cannot use a global once-flag (like a companion var) because each nativevue
* bundle load replaces the entire window.com.tencent.kuikly.core.nvi namespace object via
* its UMD wrapper. When the namespace is replaced, the previous defineProperty is lost.
* We therefore check via Object.getOwnPropertyDescriptor whether the getter is already
* installed on the *current* namespace object, and re-install if not.
*/
fun handleMultiInstanceRegisterCallNative() {
val win = kuiklyWindow.asDynamic()
val namespace = win.com.tencent.kuikly.core.nvi

if (isNullOrUndefined(namespace)) {
return
}

// Check if getter is already installed on the current namespace object.
// Must NOT use a global flag: the namespace object is replaced on every new nativevue
// bundle load, so we detect replacement and re-install the descriptor each time.
if (isDescriptorInstalled(namespace, "registerCallNative")) {
return
}

// Not installed on current namespace: save current value (if any) into next slot,
// then install the broadcast descriptor.
// registerCallNativeId starts at -1, so first ++registerCallNativeId yields 0,
// and subsequent setter calls continue incrementing from there — no slot scanning needed.
val currentValue = namespace.registerCallNative
if (!isNullOrUndefined(currentValue)) {
win[REGISTER_CALL_NATIVE_FN_KEY + "_" + (++registerCallNativeId)] = currentValue
}

// broadcast function: iterate all saved fn slots and call each one
val broadcast: (String, dynamic) -> Unit = { pagerId, callback ->
var i = 0
while (true) {
val fn = win[REGISTER_CALL_NATIVE_FN_KEY + "_" + i]
if (isNullOrUndefined(fn)) break
try {
fn(pagerId, callback)
} catch (e: dynamic) {
Log.error("registerCallNative broadcast[$i] error: $e")
}
i++
}
}

// property descriptor
val descriptor = js("{}")
descriptor.get = { broadcast }
descriptor.set = { newValue: dynamic ->
// Triggered when a new nativevue closure assigns its registerCallNative.
// Save it under the next id so the broadcast function can reach it.
if (jsTypeOf(newValue) == "function") {
win[REGISTER_CALL_NATIVE_FN_KEY + "_" + (++registerCallNativeId)] = newValue
}
}
descriptor.configurable = true
descriptor.enumerable = true

js("Object").defineProperty(namespace, "registerCallNative", descriptor)
}


/**
* handle multi-instance callKotlinMethod
*
* use Object.defineProperty to listen callKotlinMethod set,
* then save new callKotlinMethod to callKotlinMethod_id(auto increment id).
* then create dispatch method to dispatch callKotlinMethod
*/
fun handleMultiInstanceCallKotlinMethod() {
if (isInitMultiInstanceCallKotlinMethod) {
// execute once, when re init context handle. do not handle multi-instance callKotlinMethod
return
}

val win = kuiklyWindow.asDynamic()
val methodName = METHOD_NAME_CALL_KOTLIN
val currentValue = win[methodName]
// no callKotlinMethod yet: do NOT set the flag so we retry on the next init call
if (isNullOrUndefined(currentValue)) {
return
}

// Mark as installed only after confirming the value exists, mirroring the
// registerCallNative approach of not locking out a future retry prematurely.
isInitMultiInstanceCallKotlinMethod = true

// save old callKotlinMethod to callKotlinMethod_0
win[methodName + "_" + instanceId] = currentValue

// create dispatch method:use second arg(instanceId)to call special callKotlinMethod_id
val dispatch: (Int, dynamic, dynamic, dynamic, dynamic, dynamic, dynamic) -> dynamic =
{ methodOrdinal, arg0, arg1, arg2, arg3, arg4, arg5 ->
// second arg for instance id,use current instance id to call special callKotlinMethod_id
val targetKey = methodName + "_" + instanceIdMap[arg0]
val targetMethod = win[targetKey]
val defaultMethod = win[methodName + "_${instanceId}"]
if (jsTypeOf(targetMethod) == "function") {
targetMethod(methodOrdinal, arg0, arg1, arg2, arg3, arg4, arg5)
} else if (jsTypeOf(defaultMethod) == "function") {
// use default callKotlinMethod for single instance or single page multi-instance
defaultMethod(methodOrdinal, arg0, arg1, arg2, arg3, arg4, arg5)
} else {
undefined
}
}

// property descriptor
val descriptor = js("{}")
descriptor.get = { dispatch }
descriptor.set = { newValue: dynamic ->
if (jsTypeOf(newValue) == "function") {
val id = ++instanceId
win[methodName + "_" + id] = newValue
}
}
descriptor.configurable = true
descriptor.enumerable = true

// Use Object.defineProperty interception the property
js("Object").defineProperty(win, methodName, descriptor)

// expose method to decrease instance id
win[WEB_DECREASE_CALLKOTLIN_ID_METHOD] = {
instanceId--
}
}

/**
* Returns true if the given value is null or JS undefined.
*/
private fun isNullOrUndefined(value: dynamic): Boolean =
value == null || jsTypeOf(value) == "undefined"

/**
* Returns true if a getter-based property descriptor is already installed on [obj]
* for the given [propName], indicating that defineProperty has been called previously.
*/
private fun isDescriptorInstalled(obj: dynamic, propName: String): Boolean {
val desc = js("Object").getOwnPropertyDescriptor(obj, propName)
return desc != null && jsTypeOf(desc.get) == "function"
}

companion object {
private const val CALL_ARGS_COUNT = 6
private const val METHOD_NAME_CALL_NATIVE = "callNative"
private const val METHOD_NAME_CALL_KOTLIN = "callKotlinMethod"
private const val REGISTER_CALL_NATIVE_FN_KEY = "__kuikly_registerCallNative_fn"
private var isInitMultiInstanceCallKotlinMethod = false
// auto increment id for registerCallNative across closures; starts at -1 so first ++id yields slot 0
private var registerCallNativeId = -1
// multi instance auto increment id
private var instanceId = 0
private var instanceIdMap = mutableMapOf<String, Int>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ interface IKuiklyRenderCore {
* @param tag Tag corresponding to [Element]
*/
fun getView(tag: Int): Element?

/**
* Get instance id
*/
fun getInstanceId(): String
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.tencent.kuikly.core.render.web.core
import com.tencent.kuikly.core.render.web.IKuiklyRenderView
import com.tencent.kuikly.core.render.web.collection.array.JsArray
import com.tencent.kuikly.core.render.web.collection.array.fifthArg
import com.tencent.kuikly.core.render.web.collection.array.firstArg
import com.tencent.kuikly.core.render.web.collection.array.fourthArg
import com.tencent.kuikly.core.render.web.collection.array.get
import com.tencent.kuikly.core.render.web.collection.array.secondArg
Expand Down Expand Up @@ -133,6 +134,13 @@ class KuiklyRenderCore : IKuiklyRenderCore {
*/
override fun getView(tag: Int): Element? = renderLayerHandler?.getView(tag)

/**
* Get instance id
*/
override fun getInstanceId(): String {
return instanceId
}

/**
* Put event into context queue for execution
*/
Expand Down Expand Up @@ -234,7 +242,8 @@ class KuiklyRenderCore : IKuiklyRenderCore {
private fun createRenderView(method: KuiklyRenderNativeMethod, args: JsArray<Any?>): Any? {
return renderLayerHandler?.createRenderView(
args.secondArg().unsafeCast<Int>(),
args.thirdArg().unsafeCast<String>()
args.thirdArg().unsafeCast<String>(),
args.firstArg().unsafeCast<String>()
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.tencent.kuikly.core.render.web.expand.components

import com.tencent.kuikly.core.render.web.const.KRExtraConst
import com.tencent.kuikly.core.render.web.export.IKuiklyRenderViewExport
import com.tencent.kuikly.core.render.web.ktx.kuiklyDocument
import com.tencent.kuikly.core.render.web.runtime.dom.element.ElementType
import org.w3c.dom.Element
import org.w3c.dom.HTMLDivElement

/**
* KRModalView, corresponding to Kuikly Modal view.
* In web, the modal element is moved from its original parent to document.body
* to achieve full-screen overlay effect, similar to Android's addContentView.
*
* When nested KRModalViews are detected (e.g. Modal wrapping ActionSheet which
* also uses Modal internally), the inner modal stays inside the outer one
* instead of being moved to body again, preserving the correct layer hierarchy.
*/
class KRModalView : IKuiklyRenderViewExport {
// div instance
private val div = kuiklyDocument.createElement(ElementType.DIV)
// Whether the view has been moved to body
private var didMoveToWindow = false

override val reusable: Boolean
get() = false

override val ele: HTMLDivElement
get() = div.unsafeCast<HTMLDivElement>()

override fun setProp(propKey: String, propValue: Any): Boolean {
return super.setProp(propKey, propValue)
}

override fun onAddToParent(parent: Element) {
super.onAddToParent(parent)
// Move to document.body for full-screen display, similar to iOS Window / Android addContentView
if (!didMoveToWindow) {
didMoveToWindow = true
// If the current modal is already nested inside another KRModalView
// (which has already been moved to body), keep it in place to
// preserve the correct parent-child relationship.
if (isInsideModalView(parent)) {
return
}
parent.removeChild(ele)
kuiklyDocument.body?.appendChild(ele)
}
}

/**
* Walk up the ancestor chain from [parent] to check whether there is
* already a KRModalView ancestor. If so, the current modal is nested
* and should NOT be moved to body again.
*/
private fun isInsideModalView(parent: Element): Boolean {
var node: Element? = parent
val body = kuiklyDocument.body
while (node != null && node != body) {
if (node.getAttribute(KRExtraConst.COMPONENT_IDENTIFIER_KEY) == VIEW_NAME) {
return true
}
node = node.parentElement
}
return false
}

companion object {
const val VIEW_NAME = "KRModalView"
const val CONTAINER_SIZE_CHANGED = "containerSizeChanged"
}
}
Loading