Dim diff option
This commit is contained in:
195
src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt
Normal file
195
src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt
Normal file
@@ -0,0 +1,195 @@
|
||||
package nl.astraeus.komp
|
||||
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.get
|
||||
|
||||
object DiffPatch {
|
||||
|
||||
fun updateNode(oldNode: Node, newNode: Node): Node {
|
||||
if (oldNode is HTMLElement && newNode is HTMLElement) {
|
||||
if (oldNode.nodeName == newNode.nodeName) {
|
||||
if (oldNode.getAttribute("data-komp-hash") != null &&
|
||||
oldNode.getAttribute("data-komp-hash") == newNode.getAttribute("data-komp-hash")) {
|
||||
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Skip node, hash equals", oldNode, newNode)
|
||||
}
|
||||
|
||||
return oldNode
|
||||
} else {
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Update attributes", oldNode.innerHTML, newNode.innerHTML)
|
||||
}
|
||||
updateAttributes(oldNode, newNode);
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Update children", oldNode.innerHTML, newNode.innerHTML)
|
||||
}
|
||||
updateChildren(oldNode, newNode)
|
||||
updateEvents(oldNode, newNode)
|
||||
return oldNode
|
||||
}
|
||||
} else {
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Replace node ee", oldNode.innerHTML, newNode.innerHTML)
|
||||
}
|
||||
replaceNode(oldNode, newNode)
|
||||
return newNode
|
||||
}
|
||||
} else {
|
||||
if (oldNode.nodeType == newNode.nodeType && oldNode.nodeType == 3.toShort()) {
|
||||
if (oldNode.textContent != newNode.textContent) {
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Updating text content", oldNode, newNode)
|
||||
}
|
||||
oldNode.textContent = newNode.textContent
|
||||
return oldNode
|
||||
}
|
||||
}
|
||||
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Replace node", oldNode, newNode)
|
||||
}
|
||||
replaceNode(oldNode, newNode)
|
||||
return newNode
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAttributes(oldNode: HTMLElement, newNode: HTMLElement) {
|
||||
// removed attributes
|
||||
for (index in 0 until oldNode.attributes.length) {
|
||||
val attr = oldNode.attributes[index]
|
||||
|
||||
if (attr != null && newNode.attributes[attr.name] == null) {
|
||||
oldNode.removeAttribute(attr.name)
|
||||
}
|
||||
}
|
||||
|
||||
for (index in 0 until newNode.attributes.length) {
|
||||
val attr = newNode.attributes[index]
|
||||
|
||||
if (attr != null) {
|
||||
val oldAttr = oldNode.attributes[attr.name]
|
||||
|
||||
if (oldAttr == null || oldAttr.value != attr.value) {
|
||||
oldNode.setAttribute(attr.name, attr.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChildren(oldNode: HTMLElement, newNode: HTMLElement) {
|
||||
// todo: add 1 look ahead/back
|
||||
var oldIndex = 0
|
||||
var newIndex = 0
|
||||
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("updateChildren old/new count", oldNode.childNodes.length, newNode.childNodes.length)
|
||||
}
|
||||
|
||||
while(newIndex < newNode.childNodes.length) {
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log(">>> updateChildren old/new count", oldNode.childNodes, newNode.childNodes)
|
||||
console.log("Update Old/new", oldIndex, newIndex)
|
||||
}
|
||||
val newChildNode = newNode.childNodes[newIndex]
|
||||
|
||||
if (oldIndex < oldNode.childNodes.length) {
|
||||
val oldChildNode = oldNode.childNodes[oldIndex]
|
||||
|
||||
if (oldChildNode != null && newChildNode != null) {
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Update node Old/new", oldChildNode, newChildNode)
|
||||
}
|
||||
|
||||
updateNode(oldChildNode, newChildNode)
|
||||
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("--- Updated Old/new", oldNode.children, newNode.children)
|
||||
}
|
||||
} else {
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Null node", oldChildNode, newChildNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Append Old/new/node", oldIndex, newIndex, newChildNode)
|
||||
}
|
||||
oldNode.append(newChildNode)
|
||||
}
|
||||
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("<<< Updated Old/new", oldNode.children, newNode.children)
|
||||
}
|
||||
|
||||
oldIndex++
|
||||
newIndex++
|
||||
}
|
||||
|
||||
while(oldIndex < oldNode.childNodes.length) {
|
||||
oldNode.childNodes[oldIndex]?.also {
|
||||
if (Komponent.logReplaceEvent) {
|
||||
console.log("Remove old node", it)
|
||||
}
|
||||
|
||||
oldNode.removeChild(it)
|
||||
}
|
||||
oldIndex++
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEvents(oldNode: HTMLElement, newNode: HTMLElement) {
|
||||
val oldEvents = mutableListOf<String>()
|
||||
oldEvents.addAll((oldNode.getAttribute("data-komp-events") ?: "").split(","))
|
||||
|
||||
val newEvents = (newNode.getAttribute("data-komp-events") ?: "").split(",")
|
||||
|
||||
for (event in newEvents) {
|
||||
if (event.isNotBlank()) {
|
||||
val oldNodeEvent = oldNode.asDynamic()["event-$event"]
|
||||
val newNodeEvent = newNode.asDynamic()["event-$event"]
|
||||
if (oldNodeEvent != null) {
|
||||
oldNode.removeEventListener(event, oldNodeEvent as ((Event) -> Unit), null)
|
||||
}
|
||||
if (newNodeEvent != null) {
|
||||
oldNode.addEventListener(event, newNodeEvent as ((Event) -> Unit), null)
|
||||
oldNode.asDynamic()["event-$event"] = newNodeEvent
|
||||
}
|
||||
oldEvents.remove(event)
|
||||
}
|
||||
}
|
||||
|
||||
for (event in oldEvents) {
|
||||
if (event.isNotBlank()) {
|
||||
val oldNodeEvent = oldNode.asDynamic()["event-$event"]
|
||||
if (oldNodeEvent != null) {
|
||||
oldNode.removeEventListener(event, oldNodeEvent as ((Event) -> Unit), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newNode.getAttribute("data-komp-events")?.also {
|
||||
oldNode.setAttribute("data-komp-events", it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceNode(oldNode: Node, newNode: Node) {
|
||||
oldNode.parentNode?.also { parent ->
|
||||
val clone = newNode.cloneNode(true)
|
||||
if (newNode is HTMLElement) {
|
||||
val events = (newNode.getAttribute("data-komp-events") ?: "").split(",")
|
||||
for (event in events) {
|
||||
val foundEvent = newNode.asDynamic()["event-$event"]
|
||||
if (foundEvent != null) {
|
||||
clone.addEventListener(event, foundEvent as ((Event) -> Unit), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
parent.replaceChild(clone, oldNode)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
package nl.astraeus.komp
|
||||
|
||||
import kotlinx.html.DefaultUnsafe
|
||||
import kotlinx.html.Entities
|
||||
import kotlinx.html.Tag
|
||||
import kotlinx.html.TagConsumer
|
||||
import kotlinx.html.Unsafe
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.asList
|
||||
import kotlinx.html.*
|
||||
import org.w3c.dom.*
|
||||
import org.w3c.dom.css.CSSStyleDeclaration
|
||||
import org.w3c.dom.events.Event
|
||||
import kotlin.browser.document
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
private inline fun HTMLElement.setEvent(name: String, noinline callback : (Event) -> Unit) : Unit {
|
||||
asDynamic()[name] = callback
|
||||
val eventName = if (name.startsWith("on")) { name.substring(2) } else { name }
|
||||
addEventListener(eventName, callback, null)
|
||||
//asDynamic()[name] = callback
|
||||
val events = getAttribute("data-komp-events") ?: ""
|
||||
|
||||
setAttribute("data-komp-events", if (events.isBlank()) { eventName } else { "$events,$eventName" })
|
||||
asDynamic()["event-$eventName"] = callback
|
||||
}
|
||||
|
||||
interface HtmlConsumer : TagConsumer<HTMLElement> {
|
||||
@@ -73,13 +72,27 @@ class HtmlBuilder(
|
||||
}
|
||||
|
||||
override fun onTagEnd(tag: Tag) {
|
||||
var hash = 0
|
||||
if (path.isEmpty() || path.last().tagName.toLowerCase() != tag.tagName.toLowerCase()) {
|
||||
throw IllegalStateException("We haven't entered tag ${tag.tagName} but trying to leave")
|
||||
}
|
||||
|
||||
val element = path.last()
|
||||
|
||||
for (index in 0 until element.childNodes.length) {
|
||||
val child = element.childNodes[index]
|
||||
if (child is HTMLElement) {
|
||||
|
||||
hash = hash * 37 + (child.getAttribute("data-komp-hash")?.toInt() ?: 0)
|
||||
} else {
|
||||
hash = hash * 37 + (child?.textContent?.hashCode() ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
tag.attributesEntries.forEach {
|
||||
hash = hash * 37 + it.key.hashCode()
|
||||
hash = hash * 37 + it.value.hashCode()
|
||||
|
||||
if (it.key == "class") {
|
||||
val classes = it.value.split(Regex("\\s+"))
|
||||
val classNames = StringBuilder()
|
||||
@@ -121,6 +134,8 @@ class HtmlBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
element.setAttribute("data-komp-hash", hash.toString())
|
||||
|
||||
lastLeaved = path.removeAt(path.lastIndex)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ class DummyKomponent: Komponent() {
|
||||
}
|
||||
}
|
||||
|
||||
enum class UpdateStrategy {
|
||||
REPLACE,
|
||||
DOM_DIFF
|
||||
}
|
||||
|
||||
abstract class Komponent {
|
||||
var element: Node? = null
|
||||
val declaredStyles: MutableMap<String, CSSStyleDeclaration> = HashMap()
|
||||
@@ -68,10 +73,18 @@ abstract class Komponent {
|
||||
val newElement = create()
|
||||
|
||||
if (oldElement != null) {
|
||||
if (updateStrategy == UpdateStrategy.REPLACE) {
|
||||
if (logReplaceEvent) {
|
||||
console.log("Replacing", oldElement, newElement)
|
||||
}
|
||||
oldElement.parentNode?.replaceChild(newElement, oldElement)
|
||||
element = newElement
|
||||
} else {
|
||||
if (logReplaceEvent) {
|
||||
console.log("DomDiffing", oldElement, newElement)
|
||||
}
|
||||
element = DiffPatch.updateNode(oldElement, newElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +104,7 @@ abstract class Komponent {
|
||||
companion object {
|
||||
var logRenderEvent = false
|
||||
var logReplaceEvent = false
|
||||
var updateStrategy = UpdateStrategy.DOM_DIFF
|
||||
|
||||
fun create(parent: HTMLElement, component: Komponent, insertAsFirst: Boolean = false) {
|
||||
val element = component.create()
|
||||
@@ -102,4 +116,5 @@ abstract class Komponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bla</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script src="../../kotlin-js-min/main/kotlin.js"></script>
|
||||
<script src="../../kotlin-js-min/main/kotlinx-html-js.js"></script>
|
||||
<script src="../../kotlin-js-min/main/komp.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user