implement support of wasm/js for browser

This commit is contained in:
nullsanya
2025-04-03 21:22:55 +10:00
parent 8ae983971a
commit 54576d8dec
28 changed files with 811 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.common
import org.khronos.webgl.*
fun DataView.toByteArray() = ByteArray(this.byteLength) {
getInt8(it)
}
fun ArrayBuffer.toByteArray() = Int8Array(this).toByteArray()
fun ByteArray.toDataView() = DataView(ArrayBuffer(size)).also {
forEachIndexed { i, byte -> it.setInt8(i, byte) }
}
fun ByteArray.toArrayBuffer() = toDataView().buffer

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun copyToClipboard(text: String): Boolean {
return runCatching {
window.navigator.clipboard.writeText(
text
)
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

@@ -0,0 +1,31 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import kotlinx.coroutines.await
import org.w3c.fetch.Response
import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag
external class ClipboardItem(data: JsAny?) : JsAny
fun createBlobData(blob: Blob): JsAny = js("""({[blob.type]: blob})""")
inline fun Blob.convertToClipboardItem(): ClipboardItem {
return ClipboardItem(createBlobData(this))
}
suspend fun copyImageURLToClipboard(imageUrl: String): Boolean {
return runCatching {
val response = window.fetch(imageUrl).await<Response>()
val blob = response.blob().await<Blob>()
val data = arrayOf(
Blob(
arrayOf(blob).toJsArray().unsafeCast(),
BlobPropertyBag("image/png")
).convertToClipboardItem()
).toJsArray()
window.navigator.clipboard.write(data.unsafeCast())
}.onFailure {
it.printStackTrace()
}.isSuccess
}

View File

@@ -0,0 +1,63 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
private fun createMutationObserverInit(childList: Boolean, subtree: Boolean): JsAny = js("({childList, subtree})")
fun Node.onRemoved(block: () -> Unit): MutationObserver {
lateinit var observer: MutationObserver
observer = MutationObserver { _, _ ->
fun checkIfRemoved(node: Node): Boolean {
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
}
if (checkIfRemoved(this)) {
observer.disconnect()
block()
}
}
observer.observe(document, createMutationObserverInit(childList = true, subtree = true).unsafeCast())
return observer
}
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
var previousIntersectionRatio = -1f
val observer = IntersectionObserver { entries, observer ->
entries.toArray().forEach {
if (previousIntersectionRatio.toDouble() != it.intersectionRatio.toDouble()) {
previousIntersectionRatio = it.intersectionRatio.toDouble().toFloat()
it.block(previousIntersectionRatio, observer)
}
}
}
observer.observe(this)
return observer
}
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
var previous = -1f
onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio > 0 && previous == 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
var previous = -1f
return onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio == 0f && previous != 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}

View File

@@ -0,0 +1,43 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import org.w3c.dom.DOMRect
import org.w3c.dom.Element
val DOMRect.isOnScreenByLeftEdge: Boolean
get() = left >= 0 && left <= window.innerWidth
inline val Element.isOnScreenByLeftEdge
get() = getBoundingClientRect().isOnScreenByLeftEdge
val DOMRect.isOnScreenByRightEdge: Boolean
get() = right >= 0 && right <= window.innerWidth
inline val Element.isOnScreenByRightEdge
get() = getBoundingClientRect().isOnScreenByRightEdge
internal val DOMRect.isOnScreenHorizontally: Boolean
get() = isOnScreenByLeftEdge || isOnScreenByRightEdge
val DOMRect.isOnScreenByTopEdge: Boolean
get() = top >= 0 && top <= window.innerHeight
inline val Element.isOnScreenByTopEdge
get() = getBoundingClientRect().isOnScreenByTopEdge
val DOMRect.isOnScreenByBottomEdge: Boolean
get() = bottom >= 0 && bottom <= window.innerHeight
inline val Element.isOnScreenByBottomEdge
get() = getBoundingClientRect().isOnScreenByBottomEdge
internal val DOMRect.isOnScreenVertically: Boolean
get() = isOnScreenByLeftEdge || isOnScreenByRightEdge
val DOMRect.isOnScreenFully: Boolean
get() = isOnScreenByLeftEdge && isOnScreenByTopEdge && isOnScreenByRightEdge && isOnScreenByBottomEdge
val Element.isOnScreenFully: Boolean
get() = getBoundingClientRect().isOnScreenFully
val DOMRect.isOnScreen: Boolean
get() = isOnScreenFully || (isOnScreenHorizontally && isOnScreenVertically)
inline val Element.isOnScreen: Boolean
get() = getBoundingClientRect().isOnScreen

View File

@@ -0,0 +1,127 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.DOMRectReadOnly
import org.w3c.dom.Element
external interface IntersectionObserverOptions: JsAny {
/**
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
* visible.
*/
var root: Element?
/**
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
*/
var rootMargin: String?
/**
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
*/
var threshold: JsArray<JsNumber>?
}
private fun createEmptyJsObject(): JsAny = js("{}")
fun IntersectionObserverOptions(
block: IntersectionObserverOptions.() -> Unit = {}
): IntersectionObserverOptions = createEmptyJsObject().unsafeCast<IntersectionObserverOptions>().apply(block)
external interface IntersectionObserverEntry: JsAny {
/**
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
* the documentation for Element.getBoundingClientRect().
*/
val boundingClientRect: DOMRectReadOnly
/**
* Returns the ratio of the intersectionRect to the boundingClientRect.
*/
val intersectionRatio: JsNumber
/**
* Returns a DOMRectReadOnly representing the target's visible area.
*/
val intersectionRect: DOMRectReadOnly
/**
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
* then you know the transition is from intersecting to not-intersecting.
*/
val isIntersecting: Boolean
/**
* Returns a DOMRectReadOnly for the intersection observer's root.
*/
val rootBounds: DOMRectReadOnly
/**
* The Element whose intersection with the root changed.
*/
val target: Element
/**
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
* IntersectionObserver's time origin.
*/
val time: Double
}
typealias IntersectionObserverCallback = (entries: JsArray<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
/**
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
*/
external class IntersectionObserver(callback: IntersectionObserverCallback): JsAny {
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
/**
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
* was passed to the constructor or its value is null, the top-level document's viewport is used.
*/
val root: Element
/**
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
*/
val rootMargin: String
/**
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
* crossed for that target. If no value was passed to the constructor, 0 is used.
*/
val thresholds: JsArray<JsNumber>
/**
* Stops the IntersectionObserver object from observing any target.
*/
fun disconnect()
/**
* Tells the IntersectionObserver a target element to observe.
*/
fun observe(targetElement: Element)
/**
* Returns an array of IntersectionObserverEntry objects for all observed targets.
*/
fun takeRecords(): JsArray<IntersectionObserverEntry>
/**
* Tells the IntersectionObserver to stop observing a particular target element.
*/
fun unobserve(targetElement: Element)
}

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.Element
inline val Element.isOverflowWidth
get() = scrollWidth > clientWidth
inline val Element.isOverflowHeight
get() = scrollHeight > clientHeight
inline val Element.isOverflow
get() = isOverflowHeight || isOverflowWidth

View File

@@ -0,0 +1,58 @@
package dev.inmo.micro_utils.common
import kotlinx.coroutines.await
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.ErrorEvent
import org.w3c.files.*
import kotlin.js.Promise
/**
* @suppress
*/
actual typealias MPPFile = File
private fun createJsError(message: String): JsAny = js("Error(message)")
fun MPPFile.readBytesPromise() = Promise { success, failure ->
val reader = FileReader()
reader.onload = {
success((reader.result as ArrayBuffer))
}
reader.onerror = {
val message = (it as ErrorEvent).message
failure(createJsError(message))
}
reader.readAsArrayBuffer(this)
}
fun MPPFile.readBytes(): ByteArray {
val reader = FileReaderSync()
return reader.readAsArrayBuffer(this).toByteArray()
}
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await<ArrayBuffer>().toByteArray()
/**
* @suppress
*/
actual val MPPFile.filename: FileName
get() = FileName(name)
/**
* @suppress
*/
actual val MPPFile.filesize: Long
get() = jsNumberToBigInt(size).toLong()
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = ::dirtyReadBytes
private fun jsNumberToBigInt(number: JsNumber): JsBigInt = js("BigInt(number)")

View File

@@ -0,0 +1,41 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
private fun createEventListener(listener: (Event) -> Unit): JsAny = js("({handleEvent: listener})")
fun Element.onActionOutside(type: String, options: AddEventListenerOptions? = null, callback: (Event) -> Unit): EventListener {
lateinit var observer: MutationObserver
val listener = createEventListener { it: Event ->
val elementsToCheck = mutableListOf(this@onActionOutside)
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
val childrenGettingElement = elementsToCheck.removeFirst()
for (i in 0 until childrenGettingElement.childElementCount) {
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
}
}
if (elementsToCheck.isEmpty()) {
callback(it)
}
}.unsafeCast<EventListener>()
if (options == null) {
document.addEventListener(type, listener)
} else {
document.addEventListener(type, listener, options)
}
observer = onRemoved {
if (options == null) {
document.removeEventListener(type, listener)
} else {
document.removeEventListener(type, listener, options)
}
observer.disconnect()
}
return listener
}
fun Element.onClickOutside(options: AddEventListenerOptions? = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun openLink(link: String, target: String = "_blank", features: String = "") {
window.open(link, target, features) ?.focus()
}

View File

@@ -0,0 +1,56 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.*
external class ResizeObserver(
callback: (JsArray<ResizeObserverEntry>, ResizeObserver) -> Unit
): JsAny {
fun observe(target: Element, options: JsAny = definedExternally)
fun unobserve(target: Element)
fun disconnect()
}
private fun createObserveOptions(jsBox: JsString?): JsAny = js("({box: jsBox})")
external interface ResizeObserverSize: JsAny {
val blockSize: Float
val inlineSize: Float
}
external interface ResizeObserverEntry: JsAny {
val borderBoxSize: JsArray<ResizeObserverSize>
val contentBoxSize: JsArray<ResizeObserverSize>
val devicePixelContentBoxSize: JsArray<ResizeObserverSize>
val contentRect: DOMRectReadOnly
val target: Element
}
fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe(
target,
createObserveOptions(options.box?.name?.toJsString())
)
class ResizeObserverObserveOptions(
val box: Box? = null
) {
sealed interface Box {
val name: String
object Content : Box {
override val name: String
get() = "content-box"
}
object Border : Box {
override val name: String
get() = "border-box"
}
object DevicePixelContent : Box {
override val name: String
get() = "device-pixel-content-box"
}
}
}

View File

@@ -0,0 +1,30 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import kotlinx.dom.createElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.files.get
fun selectFile(
inputSetup: (HTMLInputElement) -> Unit = {},
onFailure: (Throwable) -> Unit = {},
onFile: (MPPFile) -> Unit
) {
(document.createElement("input") {
(this as HTMLInputElement).apply {
type = "file"
onchange = {
runCatching {
files ?.get(0) ?: error("File must not be null")
}.onSuccess {
onFile(it)
}.onFailure {
onFailure(it)
}
}
inputSetup(this)
}
} as HTMLElement).click()
}

View File

@@ -0,0 +1,14 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.HTMLAnchorElement
fun triggerDownloadFile(filename: String, fileLink: String) {
val hiddenElement = document.createElement("a") as HTMLAnchorElement
hiddenElement.href = fileLink
hiddenElement.target = "_blank"
hiddenElement.download = filename
hiddenElement.click()
}

View File

@@ -0,0 +1,48 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
import org.w3c.dom.Element
import org.w3c.dom.css.CSSStyleDeclaration
sealed class Visibility
data object Visible : Visibility()
data object Invisible : Visibility()
data object Gone : Visibility()
var CSSStyleDeclaration.visibilityState: Visibility
get() = when {
display == "none" -> Gone
visibility == "hidden" -> Invisible
else -> Visible
}
set(value) {
when (value) {
Visible -> {
if (display == "none") {
display = "initial"
}
visibility = "visible"
}
Invisible -> {
if (display == "none") {
display = "initial"
}
visibility = "hidden"
}
Gone -> {
display = "none"
}
}
}
inline var Element.visibilityState: Visibility
get() = window.getComputedStyle(this).visibilityState
set(value) {
window.getComputedStyle(this).visibilityState = value
}
inline val Element.isVisible: Boolean
get() = visibilityState == Visible
inline val Element.isInvisible: Boolean
get() = visibilityState == Invisible
inline val Element.isGone: Boolean
get() = visibilityState == Gone

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.common
actual fun Float.fixed(signs: Int): Float {
return jsToFixed(toDouble().toJsNumber(), signs.coerceIn(FixedSignsRange)).toString().toFloat()
}
actual fun Double.fixed(signs: Int): Double {
return jsToFixed(toJsNumber(), signs.coerceIn(FixedSignsRange)).toString().toDouble()
}
private fun jsToFixed(number: JsNumber, signs: Int): JsString = js("number.toFixed(signs)")