diff options
author | Robert Jaros <rjaros@finn.pl> | 2019-03-23 19:49:05 +0100 |
---|---|---|
committer | Robert Jaros <rjaros@finn.pl> | 2019-03-23 19:49:05 +0100 |
commit | 514d95a8f3f16b11b406bcfc9028cbf8d662227e (patch) | |
tree | 8f35315a0ee5181113592a359ddeb4b6878d91a0 | |
parent | 61649cb1b34771b3f7d9ae996040dec32c4a01c6 (diff) | |
download | kvision-514d95a8f3f16b11b406bcfc9028cbf8d662227e.tar.gz kvision-514d95a8f3f16b11b406bcfc9028cbf8d662227e.tar.bz2 kvision-514d95a8f3f16b11b406bcfc9028cbf8d662227e.zip |
CSS style objects.
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/core/Component.kt | 30 | ||||
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/core/Style.kt | 169 | ||||
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/core/Widget.kt | 16 | ||||
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/panel/Root.kt | 25 | ||||
-rw-r--r-- | src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt | 1 | ||||
-rw-r--r-- | src/test/kotlin/test/pl/treksoft/kvision/core/StyleSpec.kt | 109 |
6 files changed, 348 insertions, 2 deletions
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Component.kt b/src/main/kotlin/pl/treksoft/kvision/core/Component.kt index 766d9f9c..411eae8d 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Component.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Component.kt @@ -48,6 +48,13 @@ interface Component { fun addCssClass(css: String): Component /** + * Adds given style object to the set of CSS classes generated in html code of current component. + * @param css CSS style object + * @return current component + */ + fun addCssClass(css: Style): Component + + /** * Removes given value from the set of CSS classes generated in html code of current component. * @param css CSS class name * @return current component @@ -55,6 +62,13 @@ interface Component { fun removeCssClass(css: String): Component /** + * Removes given style object from the set of CSS classes generated in html code of current component. + * @param css CSS style object + * @return current component + */ + fun removeCssClass(css: Style): Component + + /** * Adds given value to the set of CSS classes generated in html code of parent component. * @param css CSS class name * @return current component @@ -62,6 +76,13 @@ interface Component { fun addSurroundingCssClass(css: String): Component /** + * Adds given style object to the set of CSS classes generated in html code of parent component. + * @param css CSS style object + * @return current component + */ + fun addSurroundingCssClass(css: Style): Component + + /** * Removes given value from the set of CSS classes generated in html code of parent component. * @param css CSS class name * @return current component @@ -69,6 +90,13 @@ interface Component { fun removeSurroundingCssClass(css: String): Component /** + * Removes given style object from the set of CSS classes generated in html code of parent component. + * @param css CSS style object + * @return current component + */ + fun removeSurroundingCssClass(css: Style): Component + + /** * @suppress * Internal function * Renders current component as a Snabbdom vnode. @@ -101,6 +129,7 @@ interface Component { * @return current component */ fun clearParent(): Component + /** * @suppress * Internal function. @@ -108,6 +137,7 @@ interface Component { * @return root component */ fun getRoot(): Root? + /** * @suppress * Internal function diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Style.kt b/src/main/kotlin/pl/treksoft/kvision/core/Style.kt new file mode 100644 index 00000000..7510a650 --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/core/Style.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.core + +import com.github.snabbdom.VNode +import com.github.snabbdom.h +import org.w3c.dom.Node +import pl.treksoft.jquery.JQuery +import pl.treksoft.jquery.jQuery +import pl.treksoft.kvision.panel.Root + +/** + * CSS style object. + * + * @constructor + * @param className optional name of the CSS class, it will be generated if not specified + * @param parentStyle parent CSS style object + * @param init an initializer extension function + */ +open class Style(className: String? = null, parentStyle: Style? = null, init: (Style.() -> Unit)? = null) : + StyledComponent() { + + override var parent: Container? = null + + private val newClassName: String = if (parentStyle == null) { + className ?: "kv_styleclass_${counter++}" + } else { + "${parentStyle.className} " + (className ?: ".kv_styleclass_${counter++}") + } + + /** + * The name of the CSS class. + */ + var className: String by refreshOnUpdate(newClassName) + + init { + val root = Root.getLastRoot() + parent = root + if (root != null) { + @Suppress("LeakingThis") + root.addStyle(this) + } else { + println("At least one Root object is required to create a style object!") + } + @Suppress("LeakingThis") + init?.invoke(this) + } + + override var visible: Boolean = true + set(value) { + val oldField = field + field = value + if (oldField != field) refresh() + } + + private var vnode: VNode? = null + + override fun addCssClass(css: String): Component { + return this + } + + override fun removeCssClass(css: String): Component { + return this + } + + override fun addSurroundingCssClass(css: String): Component { + return this + } + + override fun removeSurroundingCssClass(css: String): Component { + return this + } + + override fun addCssClass(css: Style): Component { + return this + } + + override fun removeCssClass(css: Style): Component { + return this + } + + override fun addSurroundingCssClass(css: Style): Component { + return this + } + + override fun removeSurroundingCssClass(css: Style): Component { + return this + } + + override fun renderVNode(): VNode { + return h("style", arrayOf(generateStyle())) + } + + internal fun generateStyle(): String { + val styles = getSnStyle() + return ".${className} {\n" + styles.map { + "${it.first}: ${it.second};" + }.joinToString("\n") + "\n}" + } + + override fun getElement(): Node? { + return this.vnode?.elm + } + + override fun getElementJQuery(): JQuery? { + return getElement()?.let { jQuery(it) } + } + + override fun getElementJQueryD(): dynamic { + return getElement()?.let { jQuery(it).asDynamic() } + } + + override fun clearParent(): Component { + this.parent = null + return this + } + + override fun getRoot(): Root? { + return this.parent?.getRoot() + } + + override fun dispose() { + } + + companion object { + internal var counter = 0 + + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun Widget.style(className: String? = null, init: (Style.() -> Unit)? = null): Style { + val style = Style(className, null, init) + this.addCssClass(style) + return style + } + + /** + * DSL builder extension function for cascading styles. + * + * It takes the same parameters as the constructor of the built component. + */ + fun Style.style(className: String? = null, init: (Style.() -> Unit)? = null): Style { + val style = Style(className, this, init) + return style + } + } + +} diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt index 9a1e556d..0d3f5c95 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt @@ -456,6 +456,22 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() { return this } + override fun addCssClass(css: Style): Widget { + return addCssClass(css.className) + } + + override fun removeCssClass(css: Style): Widget { + return removeCssClass(css.className) + } + + override fun addSurroundingCssClass(css: Style): Widget { + return addSurroundingCssClass(css.className) + } + + override fun removeSurroundingCssClass(css: Style): Widget { + return removeSurroundingCssClass(css.className) + } + override fun getElement(): Node? { return this.vnode?.elm } diff --git a/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt b/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt index 24eff10f..e069dd4c 100644 --- a/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt +++ b/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt @@ -25,6 +25,7 @@ import com.github.snabbdom.VNode import com.github.snabbdom.h import pl.treksoft.kvision.KVManager import pl.treksoft.kvision.core.StringBoolPair +import pl.treksoft.kvision.core.Style import pl.treksoft.kvision.dropdown.ContextMenu import pl.treksoft.kvision.modal.Modal import pl.treksoft.kvision.utils.snClasses @@ -44,6 +45,7 @@ import pl.treksoft.kvision.utils.snOpt * @param init an initializer extension function */ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Unit)? = null) : SimplePanel() { + private val styles: MutableList<Style> = mutableListOf() private val modals: MutableList<Modal> = mutableListOf() private val contextMenus: MutableList<ContextMenu> = mutableListOf() private var rootVnode: VNode = renderVNode() @@ -62,12 +64,18 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni return if (!fixed) { render("div#$id", arrayOf(h("div", snOpt { `class` = snClasses(listOf("row" to true)) - }, childrenVNodes() + modalsVNodes() + contextMenusVNodes()))) + }, stylesVNodes() + childrenVNodes() + modalsVNodes() + contextMenusVNodes()))) } else { - render("div#$id", childrenVNodes() + modalsVNodes() + contextMenusVNodes()) + render("div#$id", stylesVNodes() + childrenVNodes() + modalsVNodes() + contextMenusVNodes()) } } + internal fun addStyle(style: Style) { + styles.add(style) + style.parent = this + refresh() + } + internal fun addModal(modal: Modal) { modals.add(modal) modal.parent = this @@ -86,6 +94,16 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni refresh() } + private fun stylesVNodes(): Array<VNode> { + val visibleStyles = styles.filter { it.visible } + return if (visibleStyles.isNotEmpty()) { + val stylesDesc = visibleStyles.map { it.generateStyle() }.joinToString("\n") + arrayOf(h("style", arrayOf(stylesDesc))) + } else { + arrayOf() + } + } + private fun modalsVNodes(): Array<VNode> { return modals.filter { it.visible }.map { it.renderVNode() }.toTypedArray() } @@ -116,6 +134,9 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni } override fun dispose() { + styles.forEach { it.dispose() } + modals.forEach { it.dispose() } + contextMenus.forEach { it.dispose() } super.dispose() roots.remove(this) } diff --git a/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt b/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt index 9d86766c..f82dbcd3 100644 --- a/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt +++ b/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt @@ -63,6 +63,7 @@ interface DomSpec : TestSpec { val div = document.getElementById("pretest") div?.let { jQuery(it).remove() } jQuery(".modal-backdrop").remove() + Root.roots.forEach { it.dispose() } } fun assertEqualsHtml(expected: String?, actual: String?, message: String?) { diff --git a/src/test/kotlin/test/pl/treksoft/kvision/core/StyleSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/core/StyleSpec.kt new file mode 100644 index 00000000..00eb027f --- /dev/null +++ b/src/test/kotlin/test/pl/treksoft/kvision/core/StyleSpec.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package test.pl.treksoft.kvision.core + +import pl.treksoft.kvision.core.Col +import pl.treksoft.kvision.core.Color +import pl.treksoft.kvision.core.Overflow +import pl.treksoft.kvision.core.Style.Companion.style +import pl.treksoft.kvision.core.Widget.Companion.widget +import pl.treksoft.kvision.panel.Root +import pl.treksoft.kvision.utils.px +import test.pl.treksoft.kvision.DomSpec +import kotlin.browser.document +import kotlin.test.Test + +class StyleSpec : DomSpec { + + @Test + fun render() { + run { + val root = Root("test", true) { + widget { + style { + margin = 2.px + color = Color(Col.SILVER) + overflow = Overflow.SCROLL + } + } + } + root.reRender() + val element = document.getElementById("test") + assertEqualsHtml( + "<style>.kv_styleclass_0 {\noverflow: scroll;\nmargin: 2px;\ncolor: silver;\n}</style><div class=\"kv_styleclass_0\"></div>", + element?.innerHTML, + "Should render correct style element" + ) + } + } + + @Test + fun renderCustomClass() { + run { + val root = Root("test", true) { + widget { + style("customclass") { + margin = 2.px + color = Color(Col.SILVER) + overflow = Overflow.SCROLL + } + } + } + root.reRender() + val element = document.getElementById("test") + assertEqualsHtml( + "<style>.customclass {\noverflow: scroll;\nmargin: 2px;\ncolor: silver;\n}</style><div class=\"customclass\"></div>", + element?.innerHTML, + "Should render correct style element with custom class name" + ) + } + } + + @Test + fun renderSubclass() { + run { + val root = Root("test", true) { + widget { + style("customclass") { + margin = 2.px + color = Color(Col.SILVER) + overflow = Overflow.SCROLL + style("image") { + marginTop = 10.px + } + } + } + } + root.reRender() + val element = document.getElementById("test") + assertEqualsHtml( + "<style>.customclass {\noverflow: scroll;\nmargin: 2px;\ncolor: silver;\n}\n" + + ".customclass image {\n" + + "margin-top: 10px;\n" + + "}</style>" + + "<div class=\"customclass\"></div>", + element?.innerHTML, + "Should render correct child style class name" + ) + } + } +}
\ No newline at end of file |