mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2026-11-02 18:13:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9143719d31 |
No files matched your search
@@ -1,17 +1,5 @@
|
|||||||
# 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,22 +7,6 @@ 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,9 +1,7 @@
|
|||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.*
|
||||||
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
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
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.33.0
|
version=0.32.1
|
||||||
android_code_version=321
|
android_code_version=320
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "2.4.21"
|
kt = "2.4.21"
|
||||||
kt-serialization = "1.12.0"
|
kt-serialization = "1.11.0"
|
||||||
kt-coroutines = "1.11.0"
|
kt-coroutines = "1.11.0"
|
||||||
|
|
||||||
kotlinx-browser = "0.5.0"
|
kotlinx-browser = "0.5.0"
|
||||||
@@ -11,15 +11,15 @@ kslog = "2.1.0"
|
|||||||
jb-compose = "1.12.2"
|
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.6.0"
|
jb-exposed = "1.5.1"
|
||||||
jb-dokka = "2.2.0"
|
jb-dokka = "2.2.0"
|
||||||
|
|
||||||
sqlite = "3.54.1.0"
|
sqlite = "3.53.4.0"
|
||||||
|
|
||||||
korlibs = "5.4.0"
|
korlibs = "5.4.0"
|
||||||
uuid = "0.8.4"
|
uuid = "0.8.4"
|
||||||
|
|
||||||
ktor = "3.6.1"
|
ktor = "3.6.0"
|
||||||
|
|
||||||
gh-release = "2.5.2"
|
gh-release = "2.5.2"
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user