diff --git a/common/src/wasmJsMain/kotlin/ByteArrayDataView.kt b/common/src/wasmJsMain/kotlin/ByteArrayDataView.kt new file mode 100644 index 00000000000..71cb0468f89 --- /dev/null +++ b/common/src/wasmJsMain/kotlin/ByteArrayDataView.kt @@ -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) as ByteArray + +fun ByteArray.toDataView() = DataView(ArrayBuffer(size)).also { + forEachIndexed { i, byte -> it.setInt8(i, byte) } +} + +fun ByteArray.toArrayBuffer() = toDataView().buffer diff --git a/common/src/wasmJsMain/kotlin/HTMLElementDomChanged.kt b/common/src/wasmJsMain/kotlin/HTMLElementDomChanged.kt new file mode 100644 index 00000000000..825414a3587 --- /dev/null +++ b/common/src/wasmJsMain/kotlin/HTMLElementDomChanged.kt @@ -0,0 +1,61 @@ +package dev.inmo.micro_utils.common + +import kotlinx.browser.document +import org.w3c.dom.* + +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, MutationObserverInit(childList = true, subtree = true)) + return observer +} + +fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver { + var previousIntersectionRatio = -1f + val observer = IntersectionObserver { entries, observer -> + entries.forEach { + if (previousIntersectionRatio != it.intersectionRatio) { + previousIntersectionRatio = it.intersectionRatio.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 + } + } +} diff --git a/common/src/wasmJsMain/kotlin/HtmlElementOnScreen.kt b/common/src/wasmJsMain/kotlin/HtmlElementOnScreen.kt new file mode 100644 index 00000000000..93a27d3812f --- /dev/null +++ b/common/src/wasmJsMain/kotlin/HtmlElementOnScreen.kt @@ -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 diff --git a/common/src/wasmJsMain/kotlin/IsOverflow.kt b/common/src/wasmJsMain/kotlin/IsOverflow.kt new file mode 100644 index 00000000000..28412eaa0b0 --- /dev/null +++ b/common/src/wasmJsMain/kotlin/IsOverflow.kt @@ -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 diff --git a/common/src/wasmJsMain/kotlin/JSMPPFile.kt b/common/src/wasmJsMain/kotlin/JSMPPFile.kt new file mode 100644 index 00000000000..478e722e143 --- /dev/null +++ b/common/src/wasmJsMain/kotlin/JSMPPFile.kt @@ -0,0 +1,55 @@ +package dev.inmo.micro_utils.common + +import kotlinx.io.files.Path +import org.khronos.webgl.ArrayBuffer +import org.w3c.dom.ErrorEvent +import org.w3c.files.* +import kotlin.js.Promise + +/** + * @suppress + */ +actual typealias MPPFile = File + +fun MPPFile.readBytesPromise() = Promise { success, failure -> + val reader = FileReader() + reader.onload = { + success((reader.result as ArrayBuffer).toByteArray()) + Unit + } + reader.onerror = { + failure(Exception((it as ErrorEvent).message)) + Unit + } + reader.readAsArrayBuffer(this) +} + +fun MPPFile.readBytes(): ByteArray { + val reader = FileReaderSync() + return reader.readAsArrayBuffer(this).toByteArray() +} + +private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() + +/** + * @suppress + */ +actual val MPPFile.filename: FileName + get() = FileName(name) +/** + * @suppress + */ +actual val MPPFile.filesize: Long + get() = 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 diff --git a/common/src/wasmJsMain/kotlin/OpenLink.kt b/common/src/wasmJsMain/kotlin/OpenLink.kt new file mode 100644 index 00000000000..24315e95559 --- /dev/null +++ b/common/src/wasmJsMain/kotlin/OpenLink.kt @@ -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() +} + diff --git a/common/src/wasmJsMain/kotlin/SelectFile.kt b/common/src/wasmJsMain/kotlin/SelectFile.kt new file mode 100644 index 00000000000..c2af68ad32d --- /dev/null +++ b/common/src/wasmJsMain/kotlin/SelectFile.kt @@ -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() +} + diff --git a/common/src/wasmJsMain/kotlin/TriggerDownload.kt b/common/src/wasmJsMain/kotlin/TriggerDownload.kt new file mode 100644 index 00000000000..369050cf196 --- /dev/null +++ b/common/src/wasmJsMain/kotlin/TriggerDownload.kt @@ -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() +} + diff --git a/common/src/wasmJsMain/kotlin/Visibility.kt b/common/src/wasmJsMain/kotlin/Visibility.kt new file mode 100644 index 00000000000..d81fef3f27d --- /dev/null +++ b/common/src/wasmJsMain/kotlin/Visibility.kt @@ -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 +object Visible : Visibility() +object Invisible : Visibility() +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 diff --git a/common/src/wasmJsMain/kotlin/toFixed.kt b/common/src/wasmJsMain/kotlin/toFixed.kt new file mode 100644 index 00000000000..e0a2cc52f52 --- /dev/null +++ b/common/src/wasmJsMain/kotlin/toFixed.kt @@ -0,0 +1,4 @@ +package dev.inmo.micro_utils.common + +actual fun Float.fixed(signs: Int): Float = this.toJsReference().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast().toFloat() +actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast().toDouble() diff --git a/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle b/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle index 0da72807753..27676004c73 100644 --- a/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle +++ b/gradle/templates/mppJvmJsAndroidLinuxMingwLinuxArm64Project.gradle @@ -38,6 +38,11 @@ kotlin { linuxX64() mingwX64() linuxArm64() + wasm { + browser() + nodejs() + d8() + } sourceSets { commonMain { diff --git a/repos/common/tests/build.gradle b/repos/common/tests/build.gradle index 22158b48a9f..aa4a5628ce5 100644 --- a/repos/common/tests/build.gradle +++ b/repos/common/tests/build.gradle @@ -29,6 +29,11 @@ kotlin { linuxX64() mingwX64() linuxArm64() + wasm { + browser() + nodejs() + d8() + } sourceSets { commonMain { @@ -57,6 +62,10 @@ kotlin { mingwX64Main.dependsOn nativeMain linuxArm64Main.dependsOn nativeMain + if (hasProperty("wasmDependsOnJs") && wasmDependsOnJs == true) { + wasmJs.dependsOn jsMain + } + androidMain.dependsOn jvmMain } }