Files
komp/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt
2021-04-05 17:21:45 +02:00

207 lines
6.0 KiB
Kotlin

package nl.astraeus.komp
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.Node
import org.w3c.dom.get
object DiffPatch {
private fun updateKomponentOnNode(element: Node, newVdom: VDOMElement) {
val komponent = newVdom.komponent
if (komponent != null) {
if (Komponent.logReplaceEvent) {
console.log("Keeping oldNode, set oldNode element on Komponent", element, komponent)
}
komponent.element = element
}
}
fun updateNode(element: Node, oldVdom: VDOMElement, newVdom: VDOMElement): Node {
if (oldVdom.hash == newVdom.hash) {
if (Komponent.logReplaceEvent) {
console.log("Hashes match", oldVdom, newVdom, oldVdom.hash, newVdom.hash)
}
// no change
return element
}
if (oldVdom.type == newVdom.type && oldVdom.type == VDOMElementType.TEXT) {
if (oldVdom.content != newVdom.content) {
if (Komponent.logReplaceEvent) {
console.log("Updating text content", oldVdom, newVdom)
}
element.textContent = newVdom.content
}
return element
}
if (oldVdom.type == newVdom.type && oldVdom.type == VDOMElementType.TAG) {
if (oldVdom.content == newVdom.content) {
if (Komponent.logReplaceEvent) {
console.log("Update attributes", oldVdom.content, newVdom.content)
}
updateAttributes(element as HTMLElement, oldVdom, newVdom)
if (Komponent.logReplaceEvent) {
console.log("Update events", oldVdom.content, newVdom.content)
}
updateEvents(element as HTMLElement, oldVdom, newVdom)
if (Komponent.logReplaceEvent) {
console.log("Update children", oldVdom.content, newVdom.content)
}
updateKomponentOnNode(element, newVdom)
updateChildren(element, oldVdom, newVdom)
return element
}
}
if (Komponent.logReplaceEvent) {
console.log("Replace node (type)", newVdom.type, oldVdom, newVdom)
}
val newNode = newVdom.createElement()
updateKomponentOnNode(newNode, newVdom)
element.parentNode?.replaceChild(newNode, element)
return newNode
}
private fun updateAttributes(element: HTMLElement, oldVdom: VDOMElement, newVdom: VDOMElement) {
// removed attributes
for ((name, attr) in oldVdom.attributes) {
if (newVdom.attributes[name] == null) {
element.removeAttribute(name)
}
}
for ((name, value) in newVdom.attributes) {
val oldValue = oldVdom.attributes[name]
if (value != oldValue) {
element.setAttribute(name, value)
}
}
if (newVdom.content == "input" && oldVdom.content == "input") {
if (element is HTMLInputElement) {
element.value = newVdom.attributes["value"] ?: ""
element.checked = newVdom.attributes["checked"] == "true"
}
}
}
private fun updateChildren(
element: HTMLElement,
oldVdom: VDOMElement,
newVdom: VDOMElement
) {
var oldIndex = 0
var newIndex = 0
if (Komponent.logReplaceEvent) {
console.log(
"updateChildren HTML old(${oldVdom.childNodes.size})",
oldVdom.toString()
)
console.log(
"updateChildren HTML new(${newVdom.childNodes.size})",
newVdom.toString()
)
}
while (oldIndex < oldVdom.childNodes.size && newIndex < newVdom.childNodes.size) {
if (Komponent.logReplaceEvent) {
console.log("Update Old/new", oldIndex, newIndex)
}
val oldChildNode = oldVdom.childNodes[oldIndex]
val newChildNode = newVdom.childNodes[newIndex]
if (Komponent.logReplaceEvent) {
console.log("Update node Old/new", oldChildNode, newChildNode)
}
// scenarios:
// - hashes match, next
// - hashes don't match:
// -- old hash is down in new list
// --- delta == 1, insert new node
// -- new hash is down in old list
// --- delta == 1, remove current else swap?
// else: replace current node with new
if (oldVdom.hash != newVdom.hash &&
newChildNode.type == VDOMElementType.TAG &&
oldChildNode.type == VDOMElementType.TAG
) {
if (Komponent.logReplaceEvent) {
console.log("Hashes don't match")
}
val oldHash = oldChildNode.hash.hashCode()
val newHash = newChildNode.hash.hashCode()
val newHashIndexInOld = oldVdom.findNodeHashIndex(newHash)
val oldHashIndexInNew = newVdom.findNodeHashIndex(oldHash)
if (newHashIndexInOld == oldIndex + 1 && oldHashIndexInNew == newIndex) {
// remove
element.removeChild(element.childNodes[oldIndex]!!)
oldIndex++
continue
} else if (newHashIndexInOld == oldIndex && oldHashIndexInNew == newIndex + 1) {
// insert
element.insertBefore(newChildNode.createElement(), element.childNodes[oldIndex]!!)
newIndex++
oldIndex++
continue
}
}
// update
updateNode(element.childNodes[oldIndex]!!, oldChildNode, newChildNode)
oldIndex++
newIndex++
}
while (element.childNodes.length > newVdom.childNodes.size) {
element.childNodes[element.childNodes.length - 1]?.also {
if (Komponent.logReplaceEvent) {
console.log("Remove old node", it)
}
element.removeChild(it)
}
}
while (newIndex < newVdom.childNodes.size) {
newVdom.childNodes[newIndex].also {
element.appendChild(it.createElement())
}
newIndex++
}
}
private fun updateEvents(element: HTMLElement, oldVdom: VDOMElement, newVdom: VDOMElement) {
val oldEvents = oldVdom.events
val newEvents = newVdom.events
if (Komponent.logReplaceEvent) {
console.log("Update events", oldEvents, newEvents)
}
for ((name, event) in oldEvents) {
element.removeEventListener(name, event)
}
for ((name, event) in newEvents) {
if (Komponent.logReplaceEvent) {
console.log("Set event $event on", element)
}
element.addEventListener(name, event)
}
}
}