From ae3820e86fd4ac109dcec9f9b3c46467fa968139 Mon Sep 17 00:00:00 2001 From: rnentjes Date: Wed, 1 Aug 2018 19:58:56 +0200 Subject: [PATCH] Testing vdom --- build.gradle | 17 +- gradle/wrapper/gradle-wrapper.properties | 3 +- komp.iml | 11 +- komp.ipr | 20 +- src/main/kotlin/nl/astraeus/komp/DomDiff.kt | 83 +++-- .../kotlin/nl/astraeus/komp/KompElement.kt | 324 ++++++++++++++++++ src/main/kotlin/nl/astraeus/komp/Komponent.kt | 226 +++++++----- .../kotlin/nl/astraeus/komp/SizedKomponent.kt | 7 +- src/main/resources/index.html | 13 + 9 files changed, 573 insertions(+), 131 deletions(-) create mode 100644 src/main/kotlin/nl/astraeus/komp/KompElement.kt create mode 100644 src/main/resources/index.html diff --git a/build.gradle b/build.gradle index 93aaf62..c1490e1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ group 'nl.astraeus' -version '0.0.8-SNAPSHOT' +version '0.1.0-SNAPSHOT' apply plugin: 'kotlin2js' +apply plugin: 'kotlin-dce-js' apply plugin: 'idea' apply plugin: 'maven' apply plugin: 'maven-publish' @@ -20,11 +21,11 @@ repositories { } ext { - kotlin_version = '1.2.21' + kotlin_version = '1.2.51' } buildscript { - ext.kotlin_version = '1.2.21' + ext.kotlin_version = '1.2.51' repositories { maven { url "http://nexus.astraeus.nl/nexus/content/groups/public" @@ -42,6 +43,7 @@ dependencies { } uploadArchives { + println 'user: ' + nexusUsername repositories { mavenDeployer { repository(url: "http://nexus.astraeus.nl/nexus/content/repositories/releases") { @@ -52,4 +54,11 @@ uploadArchives { } } } -} \ No newline at end of file +} + +compileKotlin2Js { + kotlinOptions.sourceMap = true + kotlinOptions.sourceMapEmbedSources = "always" + + // remaining configuration options +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index be280be..d74e0cc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun Apr 15 12:16:01 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip diff --git a/komp.iml b/komp.iml index d5a90c6..a47b161 100644 --- a/komp.iml +++ b/komp.iml @@ -1,5 +1,5 @@ - + @@ -8,6 +8,8 @@ \ No newline at end of file diff --git a/komp.ipr b/komp.ipr index 7bac7c7..3e2369d 100644 --- a/komp.ipr +++ b/komp.ipr @@ -5,7 +5,6 @@ - @@ -58,7 +57,7 @@ - @@ -151,27 +150,22 @@ - - - - - - + - + - + - + - + - + diff --git a/src/main/kotlin/nl/astraeus/komp/DomDiff.kt b/src/main/kotlin/nl/astraeus/komp/DomDiff.kt index 52cb308..7764a58 100644 --- a/src/main/kotlin/nl/astraeus/komp/DomDiff.kt +++ b/src/main/kotlin/nl/astraeus/komp/DomDiff.kt @@ -1,6 +1,5 @@ package nl.astraeus.komp -import org.w3c.dom.Element import org.w3c.dom.Node import org.w3c.dom.get @@ -12,32 +11,74 @@ import org.w3c.dom.get object DomDiffer { - fun replaceDiff(newElement: Element, oldElement: Element): Element { - if (!newElement.isEqualNode(oldElement)) { - replaceNode(newElement, oldElement) + fun replaceDiff(oldElement: KompElement, newElement: KompElement, element: Node): Node { + if (oldElement.isKomponent() && newElement.isKomponent()) { + if (oldElement.equals(newElement)) { + newElement.komponent?.update() + return newElement.komponent?.element!! + } else { + return Komponent.replaceNode(newElement, element) + } + } else if (!oldElement.isKomponent() && newElement.isKomponent()) { + return Komponent.replaceNode(newElement, element) + } else if (!oldElement.equals(newElement)) { + return Komponent.replaceNode(newElement, element) + } else { + if (oldElement.children == null && newElement.children != null) { + for (index in 0 until newElement.children.size) { + Komponent.appendElement(element, newElement.children[index]) + } + } else if (oldElement.children != null && newElement.children == null) { + while(element.firstChild != null) { + element.removeChild(element.firstChild!!) + } + } else if (oldElement.children != null && newElement.children != null) { + if (oldElement.children.size > newElement.children.size) { + val toRemove = oldElement.children.size - newElement.children.size + var removed = 0 + var index = 0 - return newElement - } else { - // think of the children! - for (index in 0 until newElement.children.length) { - val newChild = newElement.children[index] - val oldChild = oldElement.children[index] + while(index < newElement.children.size) { + val childNode = element.childNodes[index] - if (newChild is Element && oldChild is Element) { - replaceDiff(newChild, oldChild) - } else { - throw IllegalStateException("Children are not nodes! $newChild, $oldChild") - } + if (childNode == null) { + println("Warn childNode is null!") + } else { + if (!oldElement.children[index + removed].equals(newElement.children[index]) && removed < toRemove) { + element.removeChild(childNode) + + removed++ + } else { + replaceDiff(oldElement.children[index + removed], newElement.children[index], childNode) + + index++ + } + } + } + + while(removed < toRemove) { + element.lastChild?.also { + Komponent.removeElement(it) } - return oldElement + removed++ + } + } else { + for (index in 0 until newElement.children.size) { + val childNode = element.childNodes[index] + + if (childNode == null) { + Komponent.appendElement(element, newElement.children[index]) + //} else if (!oldElement.children[index + removed].equals(newElement.children[index]) && removed < toRemove) { + } else { + replaceDiff(oldElement.children[index], newElement.children[index], childNode) + } + } } - } + } - private fun replaceNode(newElement: Node, oldElement: Node) { - val parent = oldElement.parentElement ?: throw IllegalStateException("oldElement has no parent! $oldElement") - - parent.replaceChild(newElement, oldElement) + return element } + } } diff --git a/src/main/kotlin/nl/astraeus/komp/KompElement.kt b/src/main/kotlin/nl/astraeus/komp/KompElement.kt new file mode 100644 index 0000000..45d5a20 --- /dev/null +++ b/src/main/kotlin/nl/astraeus/komp/KompElement.kt @@ -0,0 +1,324 @@ +package nl.astraeus.komp + +import kotlinx.html.Entities +import kotlinx.html.Tag +import kotlinx.html.TagConsumer +import kotlinx.html.Unsafe +import kotlinx.html.div +import kotlinx.html.h1 +import kotlinx.html.hr +import kotlinx.html.js.onClickFunction +import kotlinx.html.span +import org.w3c.dom.Node +import org.w3c.dom.events.Event +import kotlin.browser.document + +/** + * User: rnentjes + * Date: 8-5-18 + * Time: 21:58 + */ + +class KompElement( + val komponent: Komponent?, + var text: String, + var unsafe: Boolean = false, + val attributes: MutableMap? = null, + val children: MutableList? = null, + val events: MutableMap Unit>? = null +) { + + constructor(text: String, isText: Boolean = true) : this( + null, + text, + false, + if (isText) { + null + } else { + HashMap() + }, + if (isText) { + null + } else { + ArrayList() + }, + if (isText) { + null + } else { + HashMap() + } + ) + + constructor(komponent: Komponent) : this( + komponent, + "", + false, + null, + null, + null + ) + + /* shallow equals check */ + fun equals(other: KompElement): Boolean { + if (komponent != null) { + return komponent == other.komponent + } else if (other.isText() || isText()) { + return other.text == text + } else { + if (other.attributes?.size != attributes?.size || other.events?.size != events?.size) { + return false + } else { + (attributes?.entries)?.forEach { entry -> + if (other.attributes?.get(entry.key) != entry.value) { + return false + } + } + (events?.entries)?.forEach { entry -> + if (other.events?.get(entry.key) != entry.value) { + return false + } + } + } + } + + return true + } + + fun isText() = attributes == null && komponent == null + + fun isKomponent() = komponent != null + + fun create(): Node = when { + komponent != null -> { + komponent.element?.also { + Komponent.remove(it) + } + + val kompElement = komponent.kompElement ?: komponent.create() + val element = kompElement.create() + + komponent.kompElement = kompElement + komponent.element = element + + Komponent.define(element, komponent) + + element + } + isText() -> document.createTextNode(text) + else -> { + val result = document.createElement(text) + + (attributes?.entries)?.forEach { entry -> + result.setAttribute(entry.key, entry.value) + } + + (events?.entries)?.forEach { event -> + val key = if (event.key.startsWith("on")) { + event.key.substring(2) + } else { + event.key + } + result.addEventListener(key, event.value) + } + + children?.forEach { child -> + result.append(child.create()) + } + + result + } + } + + override fun toString(): String { + return this.toString("") + } + + fun toString(indent: String = ""): String { + val result = StringBuilder() + + if (attributes != null) { + result.append(indent) + result.append("<") + } + result.append(text) + if (attributes != null) { + for (entry in attributes.entries) { + result.append("\n") + result.append(indent) + result.append(indent) + result.append(entry.key) + result.append("=\"") + result.append(entry.value) + result.append("\"") + } + events?.apply { + for (event in this.entries) { + result.append("\n") + result.append(indent) + result.append(indent) + result.append(event.key) + result.append("=") + result.append(event.value) + } + } + result.append("\n") + result.append(indent) + result.append(">") + + children?.apply { + result.append("\n") + result.append(indent) + for (child in this) { + result.append(child.toString(" $indent")) + } + } + + result.append("\n") + result.append(indent) + result.append("") + } + + return result.toString() + } +} + +class KompConsumer : TagConsumer { + val stack = ArrayList() + var currentTag: KompElement? = null + + override fun finalize(): KompElement { + return currentTag!! + } + + override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { + //console.log("KC.onTagAttributeChange", tag, attribute, value) + if (value != null) { + currentTag?.attributes?.put(attribute, value) + } + } + + override fun onTagContent(content: CharSequence) { + //console.log("KC.onTagContent", content) + + currentTag?.children?.add(KompElement(content.toString(), true)) + } + + override fun onTagContentEntity(entity: Entities) { + console.log("KC.onTagContentEntity", entity) + } + + override fun onTagContentUnsafe(block: Unsafe.() -> Unit) { + //console.log("KC.onTagContentUnsafe", block) + + throw IllegalStateException("unsafe blocks are not supported atm.") + } + + override fun onTagEnd(tag: Tag) { + //console.log("KC.onTagEnd", tag) + + check(currentTag != null) + check(currentTag?.children != null) + + val ke = currentTag + if (stack.isNotEmpty()) { + currentTag = stack.removeAt(stack.lastIndex) + + if (ke != null) { + currentTag?.children?.add(ke) + } + } + } + + override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { + //console.log("KC.onTagEvent", tag, event, value) + + currentTag?.events?.put(event, value) + } + + override fun onTagStart(tag: Tag) { + //console.log("KC.onTagStart", tag) + + currentTag?.apply { + stack.add(this) + } + + currentTag = KompElement(tag.tagName, false) + + for (attr in tag.attributes.entries) { + currentTag?.attributes?.set(attr.key, attr.value) + } + } + + fun appendKomponent(komponent: Komponent) { + currentTag?.children?.add(KompElement(komponent)) + } + +} + +class KompTest : Komponent() { + var counter = 0 + var show = false + var child: KompTest? = null + + override fun render(consumer: KompConsumer) = consumer.div { + h1 { + +"Test" + } + hr { } + span { + +"Clicks $counter" + + onClickFunction = { + println("click") + counter++ + + update() + } + } + if (show) { + hr {} + + span { + +"Hide element" + + onClickFunction = { + show = false + + update() + } + } + + if (child == null) { + child = KompTest() + } + + include(child!!) + } else { + hr {} + + span { + +"Show element" + + onClickFunction = { + show = true + + console.log("show", this) + + update() + } + } + } + } +} + +/* +fun main(args: Array) { + val test = KompTest() + + println(test.create()) + + Komponent.create(document.body!!, KompTest()) +} +*/ diff --git a/src/main/kotlin/nl/astraeus/komp/Komponent.kt b/src/main/kotlin/nl/astraeus/komp/Komponent.kt index eb330a4..fb11780 100644 --- a/src/main/kotlin/nl/astraeus/komp/Komponent.kt +++ b/src/main/kotlin/nl/astraeus/komp/Komponent.kt @@ -1,112 +1,168 @@ package nl.astraeus.komp import kotlinx.html.HtmlBlockTag -import kotlinx.html.TagConsumer -import kotlinx.html.dom.create -import org.w3c.dom.Element import org.w3c.dom.HTMLElement -import kotlin.browser.document +import org.w3c.dom.Node +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set fun HtmlBlockTag.include(component: Komponent) { - val result = component.render(this.consumer as TagConsumer) + val consumer = this.consumer + if (consumer is KompConsumer) { + consumer.appendKomponent(component) + } - component.element = result - nl.astraeus.komp.Komponent.define(result, component) +/* + val kc = this.consumer + val result = component.render(kc as KompConsumer) + val element = result.create() + + component.element = element + Komponent.define(element, component, result) +*/ } abstract class Komponent { - var element: Element? = null - var rendered = false + var element: Node? = null + var kompElement: KompElement? = null + var rendered = false - open fun create(): HTMLElement { - var elem =element - if (elem != null) { - remove(elem) - } + open fun create(): KompElement { + val result = render(KompConsumer()) - elem = render(document.create) - rendered = true + return result + } - define(elem, this) + abstract fun render(consumer: KompConsumer): KompElement - this.element = elem + open fun refresh() { + if (!rendered) { + refresh(element) + } else { + update() + } + } - return elem + open fun update() { + refresh(element) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class.js != other::class.js) return false + + other as Komponent + + if (kompElement != other.kompElement) return false + if (rendered != other.rendered) return false + + return true + } + + override fun hashCode(): Int { + var result = kompElement?.hashCode() ?: 0 + result = 31 * result + rendered.hashCode() + return result + } + + companion object { + + private val elements: MutableMap = HashMap() + + fun replaceNode(newKomponent: KompElement, oldElement: Node): Node { + val newElement = newKomponent.create() + + val parent = oldElement.parentElement ?: throw IllegalStateException("oldElement has no parent! $oldElement") + + parent.replaceChild(newElement, oldElement) + + elements[oldElement]?.also { + elements.remove(oldElement) + } + + newKomponent.komponent?.also { + it.element = newElement + + elements[newElement] = it + } + + return newElement } - abstract fun render(consumer: TagConsumer): HTMLElement + fun removeElement(element: Node) { + val parent = element.parentElement ?: throw IllegalArgumentException("Element has no parent!?") - open fun refresh() { - if (rendered) { - refresh(element) + parent.removeChild(element) + + elements.remove(element) + } + + fun appendElement(element: Node, kompElement: KompElement) { + element.appendChild(kompElement.create()) + } + + fun define(element: Node, component: Komponent) { + elements[element] = component + } + + fun create(parent: HTMLElement, component: Komponent, insertAsFirst: Boolean = false) { + component.kompElement = component.create() + val element = component.kompElement?.create() + + if (element != null) { + if (insertAsFirst && parent.childElementCount > 0) { + parent.insertBefore(element, parent.firstChild) } else { - update() + parent.appendChild(element) } + + component.element = element + elements[element] = component + } } - open fun update() { - refresh(element) + fun remove(element: Node) { + val component = elements[element] + + elements.remove(element) } - companion object { - - private val elements: MutableMap = HashMap() - private val elementList: MutableList = ArrayList() - - fun define(element: HTMLElement, component: Komponent) { - elements[element] = component - elementList.add(component) - } - - fun create(parent: HTMLElement, component: Komponent, insertAsFirst: Boolean = false) { - val element = component.create() - - if (insertAsFirst && parent.childElementCount > 0) { - parent.insertBefore(element, parent.firstChild) - } else { - parent.appendChild(element) - } - - elements[element] = component - elementList.add(component) - } - - fun remove(element: Element) { - val component = elements[element] - - elements.remove(element) - elementList.remove(component) - } - - @JsName("remove") - fun remove(component: Komponent) { - for ((key, value) in elements) { - if (value == component) { - elements.remove(key) - } - } - elementList.remove(component) - } - - fun refresh(component: Komponent) { - refresh(component.element) - } - - fun refresh(element: Element?) { - if (element != null) { - elements[element]?.let { - //val parent = element.parentElement - val newElement = it.create() - - //parent?.replaceChild(newElement, element) - val replacedElement = DomDiffer.replaceDiff(newElement, element) - - it.element = replacedElement - - elements.remove(replacedElement) - elements[replacedElement] = it - } - } + @JsName("remove") + fun remove(component: Komponent) { + for ((key, value) in elements) { + if (value == component) { + elements.remove(key) } + } } + + fun refresh(component: Komponent) { + refresh(component.element) + } + + fun refresh(element: Node?) { + if (element != null) { + elements[element]?.let { + //val parent = element.parentElement + val newElement = it.create() + val kompElement = it.kompElement + + val replacedElement = if (kompElement != null) { + DomDiffer.replaceDiff(kompElement, newElement, element) + } else { + newElement.create() + } + + it.kompElement = newElement + it.element = replacedElement + + elements.remove(element) + elements[replacedElement] = it + + it.rendered = true + } + } + } + } } diff --git a/src/main/kotlin/nl/astraeus/komp/SizedKomponent.kt b/src/main/kotlin/nl/astraeus/komp/SizedKomponent.kt index c8f7118..20a3afe 100644 --- a/src/main/kotlin/nl/astraeus/komp/SizedKomponent.kt +++ b/src/main/kotlin/nl/astraeus/komp/SizedKomponent.kt @@ -3,7 +3,6 @@ package nl.astraeus.komp import kotlinx.html.dom.create import kotlinx.html.js.div import kotlinx.html.style -import org.w3c.dom.HTMLElement import kotlin.browser.document /** @@ -37,16 +36,20 @@ abstract class SizedKomponent( this.size = size } - override fun create(): HTMLElement { + override fun create(): KompElement { val innerResult = super.create() val result = document.create.div { style = "left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px;" // sizing here } +/* result.appendChild(innerResult) this.element = result return result +*/ + + return innerResult } } diff --git a/src/main/resources/index.html b/src/main/resources/index.html new file mode 100644 index 0000000..1477ac3 --- /dev/null +++ b/src/main/resources/index.html @@ -0,0 +1,13 @@ + + + + + Bla + + + + + + + + \ No newline at end of file