aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Component.kt30
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Style.kt169
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Widget.kt16
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/panel/Root.kt25
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt1
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/core/StyleSpec.kt109
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