mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2026-11-01 17:43:03 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a81d28426b | ||
|
|
3bd1b5da5c | ||
|
|
19ca5e1487 | ||
|
|
41772231e4 | ||
|
|
208fc4a0fc | ||
|
|
a01137f398 |
No files matched your search
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.33.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `1.5.1` -> `1.6.0`
|
||||||
|
* `Serialization`: `1.11.0` -> `1.12.0`
|
||||||
|
* `Compose`: `1.12.1` -> `1.12.2`
|
||||||
|
* `SQLite`: `3.53.4.0` -> `3.54.1.0`
|
||||||
|
* `Ktor`: `3.6.0` -> `3.6.1`
|
||||||
|
* `Common`:
|
||||||
|
* Add JS `MutationObserverInit` factory that omits unspecified options and preserves browser defaults.
|
||||||
|
* Use safe `MutationObserver` options in `onRemoved` and cover DOM observation and outside-click cleanup with browser tests.
|
||||||
|
|
||||||
## 0.32.1
|
## 0.32.1
|
||||||
|
|
||||||
* `Coroutines`:
|
* `Coroutines`:
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ plugins {
|
|||||||
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
|
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
js {
|
||||||
|
browser {
|
||||||
|
testTask {
|
||||||
|
useKarma {
|
||||||
|
useChromeHeadlessNoSandbox()
|
||||||
|
useConfigDirectory(rootProject.relativeProjectPath("gradle/karma.config.d"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodejs {
|
||||||
|
testTask {
|
||||||
|
filter.excludeTestsMatching("dev.inmo.micro_utils.common.MutationObserverBrowserTests*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import org.w3c.dom.*
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.MutationObserver
|
||||||
|
import org.w3c.dom.Node
|
||||||
|
|
||||||
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
||||||
lateinit var observer: MutationObserver
|
lateinit var observer: MutationObserver
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import org.w3c.dom.MutationObserverInit as NativeMutationObserverInit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates options for [org.w3c.dom.MutationObserver] without adding unspecified properties.
|
||||||
|
*
|
||||||
|
* Unlike the Kotlin DOM factory, this factory leaves absent options out of the JavaScript object. In particular,
|
||||||
|
* an absent [attributeFilter] must not become a null sequence, and absent [attributes] and [characterData] must not
|
||||||
|
* override the browser defaults implied by their filters or old-value options.
|
||||||
|
*
|
||||||
|
* Null arguments are omitted. Explicit false values and empty arrays are preserved. The browser remains responsible
|
||||||
|
* for validating the options passed to [org.w3c.dom.MutationObserver.observe].
|
||||||
|
*/
|
||||||
|
fun MutationObserverInit(
|
||||||
|
childList: Boolean? = null,
|
||||||
|
attributes: Boolean? = null,
|
||||||
|
characterData: Boolean? = null,
|
||||||
|
subtree: Boolean? = null,
|
||||||
|
attributeOldValue: Boolean? = null,
|
||||||
|
characterDataOldValue: Boolean? = null,
|
||||||
|
attributeFilter: Array<String>? = null
|
||||||
|
): NativeMutationObserverInit = js("{}").unsafeCast<NativeMutationObserverInit>().apply {
|
||||||
|
if (childList != null) this.childList = childList
|
||||||
|
if (attributes != null) this.attributes = attributes
|
||||||
|
if (characterData != null) this.characterData = characterData
|
||||||
|
if (subtree != null) this.subtree = subtree
|
||||||
|
if (attributeOldValue != null) this.attributeOldValue = attributeOldValue
|
||||||
|
if (characterDataOldValue != null) this.characterDataOldValue = characterDataOldValue
|
||||||
|
if (attributeFilter != null) this.attributeFilter = attributeFilter
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import org.w3c.dom.MutationObserver
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
|
import org.w3c.dom.events.EventListener
|
||||||
|
import kotlin.js.Promise
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertSame
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/** Runs only in the browser: jsNodeTest excludes this class rather than reporting DOM checks as passing. */
|
||||||
|
class MutationObserverBrowserTests {
|
||||||
|
@Test
|
||||||
|
fun childListSubtreeRecordsCanBeDrainedAndDisconnected() {
|
||||||
|
val root = document.createElement("div")
|
||||||
|
val nested = document.createElement("div")
|
||||||
|
val child = document.createElement("span")
|
||||||
|
root.appendChild(nested)
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(root, MutationObserverInit(childList = true, subtree = true))
|
||||||
|
nested.appendChild(child)
|
||||||
|
val record = observer.takeRecords().single()
|
||||||
|
assertEquals("childList", record.type)
|
||||||
|
assertSame(nested, record.target)
|
||||||
|
assertSame(child, record.addedNodes.item(0))
|
||||||
|
assertEquals(0, record.removedNodes.length)
|
||||||
|
assertNull(record.attributeName)
|
||||||
|
assertTrue(observer.takeRecords().isEmpty())
|
||||||
|
observer.disconnect()
|
||||||
|
nested.removeChild(child)
|
||||||
|
assertTrue(observer.takeRecords().isEmpty())
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun attributeFilterEnablesObservationAndPreservesOldValues() {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
target.setAttribute("data-state", "old")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(target, MutationObserverInit(attributeFilter = arrayOf("data-state"), attributeOldValue = true))
|
||||||
|
target.setAttribute("ignored", "value")
|
||||||
|
target.setAttribute("data-state", "new")
|
||||||
|
val record = observer.takeRecords().single()
|
||||||
|
assertEquals("attributes", record.type)
|
||||||
|
assertSame(target, record.target)
|
||||||
|
assertEquals("data-state", record.attributeName)
|
||||||
|
assertEquals("old", record.oldValue)
|
||||||
|
assertNull(record.attributeNamespace)
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun characterDataOldValueEnablesObservation() {
|
||||||
|
val target = document.createTextNode("old")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(target, MutationObserverInit(characterDataOldValue = true))
|
||||||
|
target.data = "new"
|
||||||
|
val record = observer.takeRecords().single()
|
||||||
|
assertEquals("characterData", record.type)
|
||||||
|
assertSame(target, record.target)
|
||||||
|
assertEquals("old", record.oldValue)
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nullAttributeFilterIsOmittedInsteadOfConvertedToASequence() {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(target, MutationObserverInit(childList = true, attributeFilter = null))
|
||||||
|
target.appendChild(document.createElement("span"))
|
||||||
|
assertEquals("childList", observer.takeRecords().single().type)
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyAttributeFilterDoesNotObserveAllAttributes() {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
observer.observe(target, MutationObserverInit(attributeFilter = emptyArray()))
|
||||||
|
target.setAttribute("data-state", "new")
|
||||||
|
assertTrue(observer.takeRecords().isEmpty())
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nativeValidationRejectsContradictoryOptions() {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
val observer = MutationObserver { _, _ -> }
|
||||||
|
try {
|
||||||
|
assertFails { observer.observe(target, MutationObserverInit()) }
|
||||||
|
assertFails {
|
||||||
|
observer.observe(target, MutationObserverInit(attributes = false, attributeFilter = arrayOf("class")))
|
||||||
|
}
|
||||||
|
assertFails {
|
||||||
|
observer.observe(target, MutationObserverInit(characterData = false, characterDataOldValue = true))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onRemovedCallsBackOnceAndDisconnects(): Promise<Unit> {
|
||||||
|
val parent = document.createElement("div")
|
||||||
|
val child = document.createElement("span")
|
||||||
|
var calls = 0
|
||||||
|
var observer: MutationObserver? = null
|
||||||
|
return Promise.resolve(Unit).then {
|
||||||
|
parent.appendChild(child)
|
||||||
|
document.body!!.appendChild(parent)
|
||||||
|
observer = child.onRemoved { calls++ }
|
||||||
|
parent.removeChild(child)
|
||||||
|
}.then {
|
||||||
|
assertEquals(1, calls)
|
||||||
|
parent.appendChild(child)
|
||||||
|
parent.removeChild(child)
|
||||||
|
}.then {
|
||||||
|
assertEquals(1, calls)
|
||||||
|
}.finally {
|
||||||
|
observer?.disconnect()
|
||||||
|
parent.parentNode?.removeChild(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onClickOutsideStopsListeningAfterRemoval(): Promise<Unit> {
|
||||||
|
val target = document.createElement("div")
|
||||||
|
var calls = 0
|
||||||
|
var listener: EventListener? = null
|
||||||
|
return Promise.resolve(Unit).then {
|
||||||
|
document.body!!.appendChild(target)
|
||||||
|
listener = target.onClickOutside { calls++ }
|
||||||
|
document.dispatchEvent(Event("click"))
|
||||||
|
assertEquals(1, calls)
|
||||||
|
target.parentNode!!.removeChild(target)
|
||||||
|
}.then {
|
||||||
|
document.dispatchEvent(Event("click"))
|
||||||
|
assertEquals(1, calls)
|
||||||
|
}.finally {
|
||||||
|
listener?.let { document.removeEventListener("click", it) }
|
||||||
|
target.parentNode?.removeChild(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import org.w3c.dom.MutationObserverInit as NativeMutationObserverInit
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertContentEquals
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertSame
|
||||||
|
|
||||||
|
class MutationObserverInitTests {
|
||||||
|
private fun keys(options: NativeMutationObserverInit): List<String> =
|
||||||
|
js("Object.keys(options)").unsafeCast<Array<String>>().sorted()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyOptionsHaveNoProperties() {
|
||||||
|
assertEquals(emptyList(), keys(MutationObserverInit()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun childListOptionsOmitUnspecifiedProperties() {
|
||||||
|
val options = MutationObserverInit(childList = true, subtree = true)
|
||||||
|
assertEquals(listOf("childList", "subtree"), keys(options))
|
||||||
|
assertEquals(true, options.childList)
|
||||||
|
assertEquals(true, options.subtree)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun explicitNullOptionsAreOmitted() {
|
||||||
|
val options = MutationObserverInit(
|
||||||
|
childList = null,
|
||||||
|
attributes = null,
|
||||||
|
characterData = null,
|
||||||
|
subtree = null,
|
||||||
|
attributeOldValue = null,
|
||||||
|
characterDataOldValue = null,
|
||||||
|
attributeFilter = null
|
||||||
|
)
|
||||||
|
assertEquals(emptyList(), keys(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun explicitFalseAndEmptyFilterArePreserved() {
|
||||||
|
val filter = emptyArray<String>()
|
||||||
|
val options = MutationObserverInit(false, false, false, false, false, false, filter)
|
||||||
|
assertEquals(
|
||||||
|
listOf("attributeFilter", "attributeOldValue", "attributes", "characterData", "characterDataOldValue", "childList", "subtree"),
|
||||||
|
keys(options)
|
||||||
|
)
|
||||||
|
assertEquals(false, options.childList)
|
||||||
|
assertEquals(false, options.attributes)
|
||||||
|
assertEquals(false, options.characterData)
|
||||||
|
assertEquals(false, options.subtree)
|
||||||
|
assertEquals(false, options.attributeOldValue)
|
||||||
|
assertEquals(false, options.characterDataOldValue)
|
||||||
|
assertSame(filter, options.attributeFilter)
|
||||||
|
assertContentEquals(emptyArray(), options.attributeFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun filtersAndOldValueOptionsDoNotForceObservationFlags() {
|
||||||
|
val filter = arrayOf("class", "data-state")
|
||||||
|
val options = MutationObserverInit(
|
||||||
|
attributeFilter = filter,
|
||||||
|
attributeOldValue = true,
|
||||||
|
characterDataOldValue = true
|
||||||
|
)
|
||||||
|
assertEquals(listOf("attributeFilter", "attributeOldValue", "characterDataOldValue"), keys(options))
|
||||||
|
assertSame(filter, options.attributeFilter)
|
||||||
|
assertEquals(true, options.attributeOldValue)
|
||||||
|
assertEquals(true, options.characterDataOldValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun eachCallCreatesIndependentOptions() {
|
||||||
|
val first = MutationObserverInit(childList = true)
|
||||||
|
val second = MutationObserverInit(attributes = true)
|
||||||
|
first.subtree = true
|
||||||
|
assertEquals(listOf("childList", "subtree"), keys(first))
|
||||||
|
assertEquals(listOf("attributes"), keys(second))
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -16,5 +16,5 @@ crypto_js_version=4.2.0
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.32.1
|
version=0.33.0
|
||||||
android_code_version=320
|
android_code_version=321
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "2.4.21"
|
kt = "2.4.21"
|
||||||
kt-serialization = "1.11.0"
|
kt-serialization = "1.12.0"
|
||||||
kt-coroutines = "1.11.0"
|
kt-coroutines = "1.11.0"
|
||||||
|
|
||||||
kotlinx-browser = "0.5.0"
|
kotlinx-browser = "0.5.0"
|
||||||
|
|
||||||
kslog = "2.1.0"
|
kslog = "2.1.0"
|
||||||
|
|
||||||
jb-compose = "1.12.1"
|
jb-compose = "1.12.2"
|
||||||
jb-compose-material3 = "1.9.0"
|
jb-compose-material3 = "1.9.0"
|
||||||
jb-compose-icons = "1.7.8"
|
jb-compose-icons = "1.7.8"
|
||||||
jb-exposed = "1.5.1"
|
jb-exposed = "1.6.0"
|
||||||
jb-dokka = "2.2.0"
|
jb-dokka = "2.2.0"
|
||||||
|
|
||||||
sqlite = "3.53.4.0"
|
sqlite = "3.54.1.0"
|
||||||
|
|
||||||
korlibs = "5.4.0"
|
korlibs = "5.4.0"
|
||||||
uuid = "0.8.4"
|
uuid = "0.8.4"
|
||||||
|
|
||||||
ktor = "3.6.0"
|
ktor = "3.6.1"
|
||||||
|
|
||||||
gh-release = "2.5.2"
|
gh-release = "2.5.2"
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ project.group = "$group"
|
|||||||
apply plugin: 'org.jetbrains.dokka'
|
apply plugin: 'org.jetbrains.dokka'
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -19,3 +19,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -19,3 +19,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -18,3 +18,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -19,3 +19,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -20,3 +20,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -31,3 +31,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
@@ -19,3 +19,7 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user