diff --git a/CHANGELOG.md b/CHANGELOG.md index b2376912b91..994767b69b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.9.9 + +* `Versions`: + * `Klock`: `2.5.1` -> `2.5.2` +* `Common`: + * Add new diff tool - `applyDiff` + * Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment) + ## 0.9.8 * `Versions`: diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt index af15eb4ca00..ab8c6d2ebc6 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt @@ -153,3 +153,22 @@ inline fun StrictDiff(old: Iterable, new: Iterable) = old.calculateDif inline fun Iterable.calculateStrictDiff( other: Iterable ) = calculateDiff(other, strictComparison = true) + +/** + * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] + * mutable list + */ +fun MutableList.applyDiff( + source: Iterable, + strictComparison: Boolean = false +) = calculateDiff(source, strictComparison).let { + for (i in it.removed.indices.sortedDescending()) { + removeAt(it.removed[i].index) + } + it.added.forEach { (i, t) -> + add(i, t) + } + it.replaced.forEach { (_, new) -> + set(new.index, new.value) + } +} diff --git a/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/DiffUtilsTests.kt b/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/DiffUtilsTests.kt index 3cfb9d4add3..251c750026b 100644 --- a/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/DiffUtilsTests.kt +++ b/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/DiffUtilsTests.kt @@ -54,7 +54,7 @@ class DiffUtilsTests { val oldList = (0 until 10).map { it.toString() } val withIndex = oldList.withIndex() - for (step in 0 until oldList.size) { + for (step in oldList.indices) { for ((i, v) in withIndex) { val mutable = oldList.toMutableList() val changes = ( @@ -73,4 +73,78 @@ class DiffUtilsTests { } } } + + @Test + fun testThatSimpleRemoveApplyWorks() { + val oldList = (0 until 10).toList() + val withIndex = oldList.withIndex() + + for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { + for ((i, _) in withIndex) { + if (i + count > oldList.lastIndex) { + continue + } + val removedSublist = oldList.subList(i, i + count) + val mutableOldList = oldList.toMutableList() + val targetList = oldList - removedSublist + + mutableOldList.applyDiff(targetList) + + assertEquals( + targetList, + mutableOldList + ) + } + } + } + + @Test + fun testThatSimpleAddApplyWorks() { + val oldList = (0 until 10).map { it.toString() } + val withIndex = oldList.withIndex() + + for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { + for ((i, v) in withIndex) { + if (i + count > oldList.lastIndex) { + continue + } + val addedSublist = oldList.subList(i, i + count).map { "added$it" } + val mutable = oldList.toMutableList() + mutable.addAll(i, addedSublist) + val mutableOldList = oldList.toMutableList() + + mutableOldList.applyDiff(mutable) + + assertEquals( + mutable, + mutableOldList + ) + } + } + } + + @Test + fun testThatSimpleChangesApplyWorks() { + val oldList = (0 until 10).map { it.toString() } + val withIndex = oldList.withIndex() + + for (step in oldList.indices) { + for ((i, v) in withIndex) { + val mutable = oldList.toMutableList() + val changes = ( + if (step == 0) i until oldList.size else (i until oldList.size step step) + ).map { index -> + IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { + mutable[index] = it.value + } + } + val mutableOldList = oldList.toMutableList() + mutableOldList.applyDiff(mutable) + assertEquals( + mutable, + mutableOldList + ) + } + } + } } diff --git a/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/IntersectionObserver.kt b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/IntersectionObserver.kt new file mode 100644 index 00000000000..3f0b7fb2c4a --- /dev/null +++ b/common/src/jsMain/kotlin/dev/inmo/micro_utils/common/IntersectionObserver.kt @@ -0,0 +1,124 @@ +package dev.inmo.micro_utils.common + +import org.w3c.dom.DOMRectReadOnly +import org.w3c.dom.Element + +external interface IntersectionObserverOptions { + /** + * 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: Array? +} +fun IntersectionObserverOptions( + block: IntersectionObserverOptions.() -> Unit = {} +): IntersectionObserverOptions = js("{}").unsafeCast().apply(block) + +external interface IntersectionObserverEntry { + /** + * 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: Number + + /** + * 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: Array, 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) { + 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: Array + + /** + * 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(): Array + + /** + * Tells the IntersectionObserver to stop observing a particular target element. + */ + fun unobserve(targetElement: Element) +} diff --git a/gradle.properties b/gradle.properties index cea669e676b..5a55491619f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ kotlin_exposed_version=0.37.3 ktor_version=1.6.7 -klockVersion=2.5.1 +klockVersion=2.5.2 github_release_plugin_version=2.2.12 @@ -45,5 +45,5 @@ dokka_version=1.6.10 # Project data group=dev.inmo -version=0.9.8 -android_code_version=98 +version=0.9.9 +android_code_version=99