Working diff option

This commit is contained in:
2020-05-05 21:12:41 +02:00
parent 7677cbcc7c
commit 70723920b3
2 changed files with 234 additions and 183 deletions

View File

@@ -2,194 +2,229 @@ package nl.astraeus.komp
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
import org.w3c.dom.get import org.w3c.dom.get
private fun NodeList.findNodeWithHash(hash: String): HTMLElement? {
for (index in 0..this.length) {
val node = this[index]
if (node is HTMLElement && node.getAttribute(DiffPatch.HASH_ATTRIBUTE) == hash) {
return node
}
}
return null
}
object DiffPatch { object DiffPatch {
const val HASH_ATTRIBUTE = "data-komp-hash"
fun updateNode(oldNode: Node, newNode: Node): Node { fun hashesMatch(oldNode: Node, newNode: Node): Boolean {
if (oldNode is HTMLElement && newNode is HTMLElement) { return (
if (oldNode.nodeName == newNode.nodeName) { oldNode is HTMLElement &&
if (oldNode.getAttribute("data-komp-hash") != null && newNode is HTMLElement &&
oldNode.getAttribute("data-komp-hash") == newNode.getAttribute("data-komp-hash")) { oldNode.nodeName == newNode.nodeName &&
oldNode.getAttribute(HASH_ATTRIBUTE) != null &&
oldNode.getAttribute(HASH_ATTRIBUTE) == newNode.getAttribute(HASH_ATTRIBUTE)
)
}
if (Komponent.logReplaceEvent) { fun updateNode(oldNode: Node, newNode: Node): Node {
console.log("Skip node, hash equals", oldNode, newNode) if (hashesMatch(oldNode, newNode)) {
} return oldNode
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) { if (oldNode.nodeType == newNode.nodeType && oldNode.nodeType == 3.toShort()) {
// removed attributes if (oldNode.textContent != newNode.textContent) {
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) { if (Komponent.logReplaceEvent) {
console.log("updateChildren old/new count", oldNode.childNodes.length, newNode.childNodes.length) console.log("Updating text content", oldNode, newNode)
} }
oldNode.textContent = newNode.textContent
return oldNode
}
}
while(newIndex < newNode.childNodes.length) { if (oldNode is HTMLElement && newNode is HTMLElement) {
if (Komponent.logReplaceEvent) { if (oldNode.nodeName == newNode.nodeName) {
console.log(">>> updateChildren old/new count", oldNode.childNodes, newNode.childNodes) if (Komponent.logReplaceEvent) {
console.log("Update Old/new", oldIndex, newIndex) console.log("Update attributes", oldNode.innerHTML, newNode.innerHTML)
} }
val newChildNode = newNode.childNodes[newIndex] updateAttributes(oldNode, newNode);
if (Komponent.logReplaceEvent) {
console.log("Update children", oldNode.innerHTML, newNode.innerHTML)
}
updateChildren(oldNode, newNode)
updateEvents(oldNode, newNode)
return oldNode
}
}
if (oldIndex < oldNode.childNodes.length) { if (Komponent.logReplaceEvent) {
val oldChildNode = oldNode.childNodes[oldIndex] console.log("Replace node", oldNode, newNode)
}
replaceNode(oldNode, newNode)
return newNode
}
if (oldChildNode != null && newChildNode != null) { private fun updateAttributes(oldNode: HTMLElement, newNode: HTMLElement) {
if (Komponent.logReplaceEvent) { // removed attributes
console.log("Update node Old/new", oldChildNode, newChildNode) for (index in 0 until oldNode.attributes.length) {
} val attr = oldNode.attributes[index]
updateNode(oldChildNode, newChildNode) if (attr != null && newNode.attributes[attr.name] == null) {
oldNode.removeAttribute(attr.name)
}
}
if (Komponent.logReplaceEvent) { for (index in 0 until newNode.attributes.length) {
console.log("--- Updated Old/new", oldNode.children, newNode.children) val attr = newNode.attributes[index]
}
} else { if (attr != null) {
if (Komponent.logReplaceEvent) { val oldAttr = oldNode.attributes[attr.name]
console.log("Null node", oldChildNode, newChildNode)
} if (oldAttr == null || oldAttr.value != attr.value) {
} oldNode.setAttribute(attr.name, attr.value)
} else { }
}
}
}
private fun updateChildren(oldNode: HTMLElement, newNode: HTMLElement) {
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 (!hashesMatch(oldChildNode, newChildNode) && newChildNode is HTMLElement && oldChildNode is HTMLElement) {
val oldHash = oldChildNode.getAttribute("data-komp-hash")
val newHash = newChildNode.getAttribute("data-komp-hash")
if (oldHash != null) {
val nodeWithHash = oldNode.childNodes.findNodeWithHash(oldHash)
if (nodeWithHash != null) {
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("Append Old/new/node", oldIndex, newIndex, newChildNode) console.log(">-> swap nodes", oldNode)
} }
oldNode.append(newChildNode)
}
if (Komponent.logReplaceEvent) { oldNode.replaceChild(oldChildNode, nodeWithHash)
console.log("<<< Updated Old/new", oldNode.children, newNode.children) oldNode.insertBefore(nodeWithHash, oldNode.childNodes[oldIndex])
}
oldIndex++
newIndex++
}
while(oldIndex < oldNode.childNodes.length) {
oldNode.childNodes[oldIndex]?.also {
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("Remove old node", it) console.log(">-> swapped nodes", oldNode)
} }
}
oldNode.removeChild(it) } else if (newHash != null) {
// if node found after current new index, insert new node
} }
oldIndex++ }
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++
} }
private fun updateEvents(oldNode: HTMLElement, newNode: HTMLElement) { while (oldIndex < oldNode.childNodes.length) {
val oldEvents = mutableListOf<String>() oldNode.childNodes[oldIndex]?.also {
oldEvents.addAll((oldNode.getAttribute("data-komp-events") ?: "").split(",")) if (Komponent.logReplaceEvent) {
console.log("Remove old node", it)
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) { oldNode.removeChild(it)
if (event.isNotBlank()) { }
val oldNodeEvent = oldNode.asDynamic()["event-$event"] oldIndex++
if (oldNodeEvent != null) { }
oldNode.removeEventListener(event, oldNodeEvent as ((Event) -> Unit), null) }
}
}
}
newNode.getAttribute("data-komp-events")?.also { private fun updateEvents(oldNode: HTMLElement, newNode: HTMLElement) {
oldNode.setAttribute("data-komp-events", it) 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)
}
} }
private fun replaceNode(oldNode: Node, newNode: Node) { for (event in oldEvents) {
oldNode.parentNode?.also { parent -> if (event.isNotBlank()) {
val clone = newNode.cloneNode(true) val oldNodeEvent = oldNode.asDynamic()["event-$event"]
if (newNode is HTMLElement) { if (oldNodeEvent != null) {
val events = (newNode.getAttribute("data-komp-events") ?: "").split(",") oldNode.removeEventListener(event, oldNodeEvent as ((Event) -> Unit), null)
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)
} }
}
} }
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)
}
}
} }

View File

@@ -7,14 +7,27 @@ import org.w3c.dom.events.Event
import kotlin.browser.document import kotlin.browser.document
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
private inline fun HTMLElement.setEvent(name: String, noinline callback : (Event) -> Unit) : Unit { private inline fun HTMLElement.setEvent(name: String, noinline callback: (Event) -> Unit): Unit {
val eventName = if (name.startsWith("on")) { name.substring(2) } else { name } val eventName = if (name.startsWith("on")) {
name.substring(2)
} else {
name
}
addEventListener(eventName, callback, null) addEventListener(eventName, callback, null)
//asDynamic()[name] = callback if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
val events = getAttribute("data-komp-events") ?: "" //asDynamic()[name] = callback
val events = getAttribute("data-komp-events") ?: ""
setAttribute("data-komp-events", if (events.isBlank()) { eventName } else { "$events,$eventName" }) setAttribute(
asDynamic()["event-$eventName"] = callback "data-komp-events",
if (events.isBlank()) {
eventName
} else {
"$events,$eventName"
}
)
asDynamic()["event-$eventName"] = callback
}
} }
interface HtmlConsumer : TagConsumer<HTMLElement> { interface HtmlConsumer : TagConsumer<HTMLElement> {
@@ -31,15 +44,15 @@ fun HTMLElement.setStyles(cssStyle: CSSStyleDeclaration) {
class HtmlBuilder( class HtmlBuilder(
val komponent: Komponent, val komponent: Komponent,
val document : Document val document: Document
) : HtmlConsumer { ) : HtmlConsumer {
private val path = arrayListOf<HTMLElement>() private val path = arrayListOf<HTMLElement>()
private var lastLeaved : HTMLElement? = null private var lastLeaved: HTMLElement? = null
override fun onTagStart(tag: Tag) { override fun onTagStart(tag: Tag) {
val element: HTMLElement = when { val element: HTMLElement = when {
tag.namespace != null -> document.createElementNS(tag.namespace!!, tag.tagName).asDynamic() tag.namespace != null -> document.createElementNS(tag.namespace!!, tag.tagName).asDynamic()
else -> document.createElement(tag.tagName) as HTMLElement else -> document.createElement(tag.tagName) as HTMLElement
} }
if (path.isNotEmpty()) { if (path.isNotEmpty()) {
@@ -51,9 +64,9 @@ class HtmlBuilder(
override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) {
when { when {
path.isEmpty() -> throw IllegalStateException("No current tag") path.isEmpty() -> throw IllegalStateException("No current tag")
path.last().tagName.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag") path.last().tagName.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag")
else -> path.last().let { node -> else -> path.last().let { node ->
if (value == null) { if (value == null) {
node.removeAttribute(attribute) node.removeAttribute(attribute)
} else { } else {
@@ -65,34 +78,36 @@ class HtmlBuilder(
override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
when { when {
path.isEmpty() -> throw IllegalStateException("No current tag") path.isEmpty() -> throw IllegalStateException("No current tag")
path.last().tagName.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag") path.last().tagName.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag")
else -> path.last().setEvent(event, value) else -> path.last().setEvent(event, value)
} }
} }
override fun onTagEnd(tag: Tag) { override fun onTagEnd(tag: Tag) {
var hash = 0 var hash: UInt = 0.toUInt()
if (path.isEmpty() || path.last().tagName.toLowerCase() != tag.tagName.toLowerCase()) { if (path.isEmpty() || path.last().tagName.toLowerCase() != tag.tagName.toLowerCase()) {
throw IllegalStateException("We haven't entered tag ${tag.tagName} but trying to leave") throw IllegalStateException("We haven't entered tag ${tag.tagName} but trying to leave")
} }
val element = path.last() val element = path.last()
for (index in 0 until element.childNodes.length) { if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
val child = element.childNodes[index] for (index in 0 until element.childNodes.length) {
if (child is HTMLElement) { val child = element.childNodes[index]
if (child is HTMLElement) {
hash = hash * 37 + (child.getAttribute("data-komp-hash")?.toInt() ?: 0) hash = hash * 37.toUInt() + (child.getAttribute(DiffPatch.HASH_ATTRIBUTE)?.toUInt(16) ?: 0.toUInt())
} else { } else {
hash = hash * 37 + (child?.textContent?.hashCode() ?: 0) hash = hash * 37.toUInt() + (child?.textContent?.hashCode()?.toUInt() ?: 0.toUInt())
}
} }
} }
tag.attributesEntries.forEach { tag.attributesEntries.forEach {
hash = hash * 37 + it.key.hashCode() val key_value = "${it.key}-${it.value}"
hash = hash * 37 + it.value.hashCode() if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
hash = hash * 37.toUInt() + key_value.hashCode().toUInt()
}
if (it.key == "class") { if (it.key == "class") {
val classes = it.value.split(Regex("\\s+")) val classes = it.value.split(Regex("\\s+"))
val classNames = StringBuilder() val classNames = StringBuilder()
@@ -134,8 +149,9 @@ class HtmlBuilder(
} }
} }
element.setAttribute("data-komp-hash", hash.toString()) if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
element.setAttribute(DiffPatch.HASH_ATTRIBUTE, hash.toString(16))
}
lastLeaved = path.removeAt(path.lastIndex) lastLeaved = path.removeAt(path.lastIndex)
} }
@@ -187,7 +203,7 @@ class HtmlBuilder(
private fun HTMLElement.asR(): HTMLElement = this.asDynamic() private fun HTMLElement.asR(): HTMLElement = this.asDynamic()
companion object { companion object {
fun create(content: HtmlBuilder.() -> Unit) : HTMLElement { fun create(content: HtmlBuilder.() -> Unit): HTMLElement {
val consumer = HtmlBuilder(DummyKomponent(), document) val consumer = HtmlBuilder(DummyKomponent(), document)
content.invoke(consumer) content.invoke(consumer)
return consumer.finalize() return consumer.finalize()