compose-webview-multiplatform icon indicating copy to clipboard operation
compose-webview-multiplatform copied to clipboard

How to implement in wasmjs kotlin

Open rohan-paudel opened this issue 9 months ago • 11 comments

rohan-paudel avatar Feb 06 '25 02:02 rohan-paudel

ok I implemented this using some code.... I will give here code ... Please someone include in this library.....

rohan-paudel avatar Feb 08 '25 15:02 rohan-paudel

package com.saralapps.common.handler.wasm

import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.SnapshotStateObserver import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusTarget import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.round import kotlinx.browser.document import kotlinx.dom.createElement import org.w3c.dom.Document import org.w3c.dom.Element import kotlin.contracts.ExperimentalContracts

val NoOpUpdate: Element.() -> Unit = {}

private class ComponentInfo<T : Element> { lateinit var container: Element lateinit var component: T lateinit var updater: Updater<T> }

private class FocusSwitcher<T : Element>( private val info: ComponentInfo<T>, private val focusManager: FocusManager ) { private val backwardRequester = FocusRequester() private val forwardRequester = FocusRequester() private var isRequesting = false

fun moveBackward() {
    try {
        isRequesting = true
        backwardRequester.requestFocus()
    } finally {
        isRequesting = false
    }
    focusManager.moveFocus(FocusDirection.Previous)
}

fun moveForward() {
    try {
        isRequesting = true
        forwardRequester.requestFocus()
    } finally {
        isRequesting = false
    }
    focusManager.moveFocus(FocusDirection.Next)
}

@Composable
fun Content() {
    Box(
        Modifier
            .focusRequester(backwardRequester)
            .onFocusChanged {
                if (it.isFocused && !isRequesting) {
                    focusManager.clearFocus(force = true)
                    val component = info.container.firstElementChild
                    if(component != null) {
                        requestFocus(component)
                    }else {
                        moveForward()
                    }
                }
            }
            .focusTarget()
    )
    Box(
        Modifier
            .focusRequester(forwardRequester)
            .onFocusChanged {
                if (it.isFocused && !isRequesting) {
                    focusManager.clearFocus(force = true)

                    val component = info.container.lastElementChild
                    if(component != null) {
                        requestFocus(component)
                    }else {
                        moveBackward()
                    }
                }
            }
            .focusTarget()
    )
}

}

private fun requestFocus(element: Element) : Unit = js(""" { element.focus(); } """)

private fun initializingElement(element: Element) : Unit = js(""" { element.style.position = 'absolute'; element.style.margin = '0px'; } """)

private fun changeCoordinates(element: Element,width: Float,height: Float,x: Float,y: Float) : Unit = js(""" { element.style.width = width + 'px'; element.style.height = height + 'px'; element.style.left = x + 'px'; element.style.top = y + 'px'; } """)

@OptIn(ExperimentalContracts::class) @Composable fun <T : Element> HtmlView( factory: Document.() -> T, modifier: Modifier = Modifier, update: (T) -> Unit = NoOpUpdate ) {

val componentInfo = remember { ComponentInfo<T>() }

val root = LocalLayerContainer.current
val density = LocalDensity.current.density
val focusManager = LocalFocusManager.current
val focusSwitcher = remember { FocusSwitcher(componentInfo, focusManager) }

Box(
    modifier = modifier.onGloballyPositioned { coordinates ->
        val location = coordinates.positionInWindow().round()
        val size = coordinates.size
        changeCoordinates(componentInfo.component,size.width / density, size.height / density, location.x / density,location.y / density)
    }
) {
    focusSwitcher.Content()
}

DisposableEffect(factory) {
    componentInfo.container = document.createElement("div",NoOpUpdate)
    componentInfo.component = document.factory()
    root.insertBefore(componentInfo.container,root.firstChild)
    componentInfo.container.append(componentInfo.component)
    componentInfo.updater = Updater(componentInfo.component, update)
    initializingElement(componentInfo.component)
    onDispose {
        root.removeChild(componentInfo.container)
        componentInfo.updater.dispose()
    }
}

SideEffect {
    componentInfo.updater.update = update
}

}

private class Updater<T : Element>( private val component: T, update: (T) -> Unit ) { private var isDisposed = false

private val snapshotObserver = SnapshotStateObserver { command ->
    command()
}

private val scheduleUpdate = { _: T ->
    if(isDisposed.not()) {
        performUpdate()
    }
}

var update: (T) -> Unit = update
    set(value) {
        if (field != value) {
            field = value
            performUpdate()
        }
    }

private fun performUpdate() {
    snapshotObserver.observeReads(component, scheduleUpdate) {
        update(component)
    }
}

init {
    snapshotObserver.start()
    performUpdate()
}

fun dispose() {
    snapshotObserver.stop()
    snapshotObserver.clear()
    isDisposed = true
}

}

import androidx.compose.runtime.staticCompositionLocalOf import org.w3c.dom.Element

val LocalLayerContainer = staticCompositionLocalOf<Element> { error("CompositionLocal LayerContainer not provided") // you can replace this with document.body!! }

this is HTML View that provides

HtmlView(
    modifier = Modifier.fillMaxSize(),
    factory = {

}

)

to render html code in wasm.......

and inside factoory i created iframe...

like below

factory = { val iFrame = createElement("iframe") val url = window.location.host iFrame.setAttribute("srcdoc", """

PDF.js viewer Hey""")

iFrame }

rohan-paudel avatar Feb 08 '25 15:02 rohan-paudel

using this i rendered PDF in wasm as well using pdf.js "Javascript library".

rohan-paudel avatar Feb 08 '25 15:02 rohan-paudel

Image Image

rohan-paudel avatar Feb 08 '25 15:02 rohan-paudel

only pdf portion is html and js and other is compose multiplatform

rohan-paudel avatar Feb 08 '25 15:02 rohan-paudel

I checked this code @rohan-paudel and it is working, great job there. I created a container name it IWebView and use WebViewKMP for mobile and desktop and use your solution (HtmlView) for wasmjs

Here is my working code:

package app.ybee.ui.core.common.component

import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateObserver
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.round
import kotlinx.browser.document
import org.w3c.dom.Document
import org.w3c.dom.Element
import kotlin.contracts.ExperimentalContracts


val NoOpUpdate: Element.() -> Unit = {}

val LocalLayerContainer = staticCompositionLocalOf {
    document.body!!
}

private class ComponentInfo<T : Element> {
    lateinit var container: Element
    lateinit var component: T
    lateinit var updater: Updater<T>
}

private class FocusSwitcher<T : Element>(
    private val info: ComponentInfo<T>,
    private val focusManager: FocusManager
) {
    private val backwardRequester = FocusRequester()
    private val forwardRequester = FocusRequester()
    private var isRequesting = false

    fun moveBackward() {
        try {
            isRequesting = true
            backwardRequester.requestFocus()
        } finally {
            isRequesting = false
        }
        focusManager.moveFocus(FocusDirection.Previous)
    }

    fun moveForward() {
        try {
            isRequesting = true
            forwardRequester.requestFocus()
        } finally {
            isRequesting = false
        }
        focusManager.moveFocus(FocusDirection.Next)
    }

    @Composable
    fun Content() {
        Box(
            Modifier
                .focusRequester(backwardRequester)
                .onFocusChanged {
                    if (it.isFocused && !isRequesting) {
                        focusManager.clearFocus(force = true)
                        val component = info.container.firstElementChild
                        if(component != null) {
                            requestFocus(component)
                        }else {
                            moveForward()
                        }
                    }
                }
                .focusTarget()
        )
        Box(
            Modifier
                .focusRequester(forwardRequester)
                .onFocusChanged {
                    if (it.isFocused && !isRequesting) {
                        focusManager.clearFocus(force = true)

                        val component = info.container.lastElementChild
                        if(component != null) {
                            requestFocus(component)
                        }else {
                            moveBackward()
                        }
                    }
                }
                .focusTarget()
        )
    }
}

private fun requestFocus(element: Element) : Unit = js("""
{
element.focus();
}
""")

private fun initializingElement(element: Element) : Unit = js("""
{
element.style.position = 'absolute';
element.style.margin = '0px';
}
""")

private fun changeCoordinates(element: Element,width: Float,height: Float,x: Float,y: Float) : Unit = js("""
{
element.style.width = width + 'px';
element.style.height = height + 'px';
element.style.left = x + 'px';
element.style.top = y + 'px';
}
""")

@OptIn(ExperimentalContracts::class)
@Composable
fun <T : Element> HtmlView(
    factory: Document.() -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
) {

    val componentInfo = remember { ComponentInfo<T>() }

    val root = LocalLayerContainer.current
    val density = LocalDensity.current.density
    val focusManager = LocalFocusManager.current
    val focusSwitcher = remember { FocusSwitcher(componentInfo, focusManager) }

    Box(
        modifier = modifier.onGloballyPositioned { coordinates ->
            val location = coordinates.positionInWindow().round()
            val size = coordinates.size
            changeCoordinates(componentInfo.component,size.width / density, size.height / density, location.x / density,location.y / density)
        }
    ) {
        focusSwitcher.Content()
    }

    DisposableEffect(factory) {
        componentInfo.container = document.createElement("div")
        componentInfo.component = factory(document)
        (root as Element).insertBefore(componentInfo.container, (root as Element).firstChild)
        componentInfo.container.append(componentInfo.component)
        componentInfo.updater = Updater(componentInfo.component, update)
        initializingElement(componentInfo.component)
        onDispose {
            (root as Element).removeChild(componentInfo.container)
            componentInfo.updater.dispose()
        }
    }

    SideEffect {
        componentInfo.updater.update = update
    }
}

private class Updater<T : Element>(
    private val component: T,
    update: (T) -> Unit
) {
    private var isDisposed = false

    private val snapshotObserver = SnapshotStateObserver { command ->
        command()
    }

    private val scheduleUpdate = { _: T ->
        if(isDisposed.not()) {
            performUpdate()
        }
    }

    var update: (T) -> Unit = update
        set(value) {
            if (field != value) {
                field = value
                performUpdate()
            }
        }

    private fun performUpdate() {
        snapshotObserver.observeReads(component, scheduleUpdate) {
            update(component)
        }
    }

    init {
        snapshotObserver.start()
        performUpdate()
    }

    fun dispose() {
        snapshotObserver.stop()
        snapshotObserver.clear()
        isDisposed = true
    }
}

And here is I used it in my IWebView:

@Composable
actual fun IWebView(
    source: String,
    isHtmlData: Boolean,
    modifier: Modifier
) {
    if (isHtmlData) {
        HtmlView(
            factory = {
                createElement("iframe") as org.w3c.dom.HTMLIFrameElement
            },
            modifier = modifier,
            update = { iframe ->
                iframe.srcdoc = source
                iframe.style.border = "none"
                iframe.style.width = "100%"
                iframe.style.height = "100%"
            }
        )
    } else {
        HtmlView(
            factory = {
                createElement("iframe") as org.w3c.dom.HTMLIFrameElement
            },
            modifier = modifier,
            update = { iframe ->
                iframe.src = source
                iframe.style.border = "none"
                iframe.style.width = "100%"
                iframe.style.height = "100%"
            }
        )
    }
}

@KevinnZou, maybe you can consider this as a solution for Web as well

amirghm avatar May 16 '25 09:05 amirghm

Thanks to you @rohan-paudel, I used it and implemented a wasmJs support for this library

You can find it here

amirghm avatar May 27 '25 00:05 amirghm

@amirghm Brother, before publish also make sure in iOS whole screen is used by webview... Currently in iOS it is auto audjusting to safe area..... I think from package it should support edge to edge and package user should handle if they dont need edge to edge by giving padding..... Simple

rohan-paudel avatar Jun 02 '25 15:06 rohan-paudel

Hey @rohan-paudel , Actually I didn't changed anything for this part, but there maybe more updates, I will be checking that one as well

amirghm avatar Jun 02 '25 16:06 amirghm

Currently in iOS it is auto adjusting to safe area..... for this, you need to set ignoreSafeAreafor your iOS project to let views draw full screen. check your iosApp project for this

amirghm avatar Jun 04 '25 12:06 amirghm

Image

are you talking about that bro? Already did,,,, whole app is ignoring the safe area,,,,, but if I open the screen containing WebView, than just that WebView shifts to down to safe area

rohan-paudel avatar Jun 05 '25 02:06 rohan-paudel