From 7677cbcc7ca298890e82c029445013f9190fea15 Mon Sep 17 00:00:00 2001 From: rnentjes Date: Mon, 4 May 2020 19:14:04 +0200 Subject: [PATCH] Dim diff option --- build.gradle | 82 -------- build.gradle.kts | 77 +++++++ komp.iml | 33 +-- komp.ipr | 5 +- komp_test.iml | 51 ++--- settings.gradle | 3 - settings.gradle.kts | 16 ++ .../kotlin/nl/astraeus/komp/DiffPatch.kt | 195 ++++++++++++++++++ .../kotlin/nl/astraeus/komp/HtmlBuilder.kt | 35 +++- .../kotlin/nl/astraeus/komp/Komponent.kt | 15 ++ src/main/resources/index.html | 13 -- 11 files changed, 347 insertions(+), 178 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts create mode 100644 src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt rename src/{main => jsMain}/kotlin/nl/astraeus/komp/HtmlBuilder.kt (85%) rename src/{main => jsMain}/kotlin/nl/astraeus/komp/Komponent.kt (86%) delete mode 100644 src/main/resources/index.html diff --git a/build.gradle b/build.gradle deleted file mode 100644 index eb5b122..0000000 --- a/build.gradle +++ /dev/null @@ -1,82 +0,0 @@ -buildscript { - ext.kotlin_version = '1.3.70' - repositories { - maven { - url "http://nexus.astraeus.nl/nexus/content/groups/public" - } - mavenCentral() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -plugins { - id("org.jetbrains.kotlin.js") version "1.3.70" -} - -apply plugin: 'idea' -apply plugin: 'maven' -apply plugin: 'maven-publish' - -group 'nl.astraeus' -version '0.1.17-SNAPSHOT' -/* - -kotlin { - target { - browser { - webpackTask { - output.libraryTarget = "umd" - } - } - } -} - -compileKotlinJs.kotlinOptions.moduleKind = "umd" -*/ - -idea { - module { - name = "komp" - } -} - -repositories { - maven { - url "http://nexus.astraeus.nl/nexus/content/groups/public" - } - mavenCentral() -} - -ext { - kotlin_version = '1.3.70' -} - -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" - compile 'org.jetbrains.kotlinx:kotlinx-html-js:0.7.1' -} - -uploadArchives { - //println 'user: ' + nexusUsername - repositories { - mavenDeployer { - repository(url: "http://nexus.astraeus.nl/nexus/content/repositories/releases") { - authentication(userName: nexusUsername, password: nexusPassword) - } - snapshotRepository(url: "http://nexus.astraeus.nl/nexus/content/repositories/snapshots") { - authentication(userName: nexusUsername, password: nexusPassword) - } - } - } -} - -/* -compileKotlin2Js { - kotlinOptions.sourceMap = true - kotlinOptions.sourceMapEmbedSources = "always" - - // remaining configuration options -} -*/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..0483079 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,77 @@ +plugins { + kotlin("multiplatform") version "1.4-M2-eap-68" + `maven-publish` +} + +group = "nl.astraeus" +version = "0.1.20-SNAPSHOT" + +repositories { + maven { setUrl("https://dl.bintray.com/kotlin/kotlin-eap") } + mavenCentral() + maven { + url = uri("https://dl.bintray.com/kotlin/kotlin-dev") + } +} + +kotlin { + /* Targets configuration omitted. + * To find out how to configure the targets, please follow the link: + * https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#setting-up-targets */ + js { + browser { + //produceKotlinLibrary() + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(kotlin("stdlib-common")) + + //implementation("org.jetbrains.kotlinx:kotlinx-html:0.7.2-build-1711") + } + } + val jsMain by getting { + dependencies { + implementation(kotlin("stdlib-js")) + + api("org.jetbrains.kotlinx:kotlinx-html-js:0.7.2-build-1716") + } + } + } +} + +publishing { + repositories { + maven { + name = "releases" + // change to point to your repo, e.g. http://my.org/repo + url = uri("http://nexus.astraeus.nl/nexus/content/repositories/releases") + credentials { + val nexusUsername: String by project + val nexusPassword: String by project + + username = nexusUsername + password = nexusPassword + } + } + maven { + name = "snapshots" + // change to point to your repo, e.g. http://my.org/repo + url = uri("http://nexus.astraeus.nl/nexus/content/repositories/snapshots") + credentials { + val nexusUsername: String by project + val nexusPassword: String by project + + username = nexusUsername + password = nexusPassword + } + } + } + publications { + val kotlinMultiplatform by getting { + //artifactId = "kotlin-css-generator" + } + } +} diff --git a/komp.iml b/komp.iml index 22881c0..910794e 100644 --- a/komp.iml +++ b/komp.iml @@ -1,41 +1,12 @@ - - - - - $MODULE_DIR$/build/classes/kotlin/test/komp_test.js - - - - - - - - - + - - + \ No newline at end of file diff --git a/komp.ipr b/komp.ipr index 503d6d5..46cb162 100644 --- a/komp.ipr +++ b/komp.ipr @@ -27,6 +27,9 @@ + + + @@ -218,7 +221,7 @@ - + diff --git a/komp_test.iml b/komp_test.iml index 65742cb..ccc6890 100644 --- a/komp_test.iml +++ b/komp_test.iml @@ -39,6 +39,17 @@ + + + + + + + + + + + @@ -48,45 +59,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index fadcfdf..0000000 --- a/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -rootProject.name = 'komp' - -enableFeaturePreview('GRADLE_METADATA') diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..812c733 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + + maven { setUrl("https://dl.bintray.com/kotlin/kotlin-dev") } + + maven { setUrl("https://dl.bintray.com/kotlin/kotlin-eap") } + + mavenCentral() + + maven { setUrl("https://plugins.gradle.org/m2/") } + } +} + +rootProject.name = "komp" + +enableFeaturePreview("GRADLE_METADATA") diff --git a/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt b/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt new file mode 100644 index 0000000..5e934dd --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt @@ -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() + 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) + } + } + +} diff --git a/src/main/kotlin/nl/astraeus/komp/HtmlBuilder.kt b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt similarity index 85% rename from src/main/kotlin/nl/astraeus/komp/HtmlBuilder.kt rename to src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt index b6415df..3e685f2 100644 --- a/src/main/kotlin/nl/astraeus/komp/HtmlBuilder.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt @@ -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 { @@ -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) } diff --git a/src/main/kotlin/nl/astraeus/komp/Komponent.kt b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt similarity index 86% rename from src/main/kotlin/nl/astraeus/komp/Komponent.kt rename to src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt index 3521be4..22cf766 100644 --- a/src/main/kotlin/nl/astraeus/komp/Komponent.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt @@ -33,6 +33,11 @@ class DummyKomponent: Komponent() { } } +enum class UpdateStrategy { + REPLACE, + DOM_DIFF +} + abstract class Komponent { var element: Node? = null val declaredStyles: MutableMap = 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 { } } } + } diff --git a/src/main/resources/index.html b/src/main/resources/index.html deleted file mode 100644 index 1477ac3..0000000 --- a/src/main/resources/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Bla - - - - - - - - \ No newline at end of file