aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/assets/img/kotlin.pngbin0 -> 5966 bytes
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/ApplicationBase.kt (renamed from src/main/kotlin/pl/treksoft/kvision/Application.kt)0
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/Main.kt (renamed from src/main/kotlin/pl/treksoft/kvision/main.kt)15
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/MainApplication.kt11
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/Showcase.kt102
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/basic/Label.kt7
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Container.kt32
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Img.kt10
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/KVManager.kt34
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/KVObject.kt3
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Root.kt27
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Widget.kt121
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt55
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/Button.kt99
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/Image.kt66
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/InTag.kt48
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/List.kt62
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/Tag.kt69
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/routing/Routing.kt7
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/snabbdom/Types.kt57
-rw-r--r--src/main/web/index.html14
-rw-r--r--src/test/kotlin/test/TestMainApplication.kt8
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt48
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/core/ContainerSpec.kt68
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/core/KVManagerSpec.kt58
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/core/RootSpec.kt29
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/core/WidgetSpec.kt89
27 files changed, 1108 insertions, 31 deletions
diff --git a/src/main/assets/img/kotlin.png b/src/main/assets/img/kotlin.png
new file mode 100644
index 00000000..4ac96a24
--- /dev/null
+++ b/src/main/assets/img/kotlin.png
Binary files differ
diff --git a/src/main/kotlin/pl/treksoft/kvision/Application.kt b/src/main/kotlin/pl/treksoft/kvision/ApplicationBase.kt
index a7b3d465..a7b3d465 100644
--- a/src/main/kotlin/pl/treksoft/kvision/Application.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/ApplicationBase.kt
diff --git a/src/main/kotlin/pl/treksoft/kvision/main.kt b/src/main/kotlin/pl/treksoft/kvision/Main.kt
index 01ce118f..918cd51f 100644
--- a/src/main/kotlin/pl/treksoft/kvision/main.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/Main.kt
@@ -26,15 +26,10 @@ fun main(args: Array<String>) {
}
fun start(state: dynamic): ApplicationBase? {
- if (document.body?.hasClass("kvision") ?: false) {
- val application = MainApplication()
-
- @Suppress("UnsafeCastFromDynamic")
- application.start(state?.appState ?: emptyMap())
-
- return application
- } else {
- return null
- }
+ if (document.getElementById("showcase") == null) return null
+ val application = Showcase()
+ @Suppress("UnsafeCastFromDynamic")
+ application.start(state?.appState ?: emptyMap())
+ return application
}
diff --git a/src/main/kotlin/pl/treksoft/kvision/MainApplication.kt b/src/main/kotlin/pl/treksoft/kvision/MainApplication.kt
deleted file mode 100644
index 92bcca53..00000000
--- a/src/main/kotlin/pl/treksoft/kvision/MainApplication.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package pl.treksoft.kvision
-
-import kotlin.browser.*
-
-class MainApplication : ApplicationBase() {
-
- override fun start(state: Map<String, Any>) {
- }
-
- override fun dispose() = mapOf<String, Any>()
-} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/Showcase.kt b/src/main/kotlin/pl/treksoft/kvision/Showcase.kt
new file mode 100644
index 00000000..caa66269
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/Showcase.kt
@@ -0,0 +1,102 @@
+package pl.treksoft.kvision
+
+import pl.treksoft.kvision.basic.Label
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.Img
+import pl.treksoft.kvision.core.KVManager
+import pl.treksoft.kvision.core.Root
+import pl.treksoft.kvision.dropdown.DropDown
+import pl.treksoft.kvision.html.*
+import pl.treksoft.kvision.html.TAG.H1
+import pl.treksoft.kvision.routing.routing
+
+class Showcase : ApplicationBase() {
+
+ override fun start(state: Map<String, Any>) {
+ val root = Root("showcase")
+ val container = Container(setOf("abc", "def"))
+ val h1 = Tag(H1, "To jest <i>test pisania</i> tekstu", false, ALIGN.NONE, classes = setOf("test", "test2"))
+ container.add(h1)
+ val label = Label("KVLabel1")
+ container.add(label)
+ val label2 = Label("KVLabel2")
+ container.add(label2)
+ root.add(container)
+ label.hide()
+ label.show()
+
+ val dd = DropDown("Dropdown", listOf("DAsdasd asdasdas das", "dasdasd asdasdas das"), "flag")
+ root.add(dd)
+
+ val p = Tag(TAG.P, "To jest prawo", align = ALIGN.RIGHT)
+ p.title = "Tytuł"
+ root.add(p)
+ val del = InTag(INTAG.DEL, "To jest deleted")
+ root.add(del)
+
+ val list = ListTag(LIST.DL_HORIZ, listOf("abc", "de<b>fdasdasdasddasd</b>tdasdas", "Dasdsada", "dasdasdads"), true)
+ root.add(list)
+
+ val img = Image(Img("kotlin.png"), "Image", true, IMAGE_SHAPE.ROUNDED)
+ root.add(img)
+
+ val button = Button("To jest przycisk FA", "fa-flag", BUTTON_STYLE.DANGER)
+ button.setEventListener<Button> {
+ click = { _ -> println(self.text) }
+ }
+ root.add(button)
+ val button2 = Button("To jest przycisk", "flag", BUTTON_STYLE.DANGER)
+ button2.setEventListener {
+ click = { e ->
+ println("2" + e)
+ button.setEventListener {
+ click = null
+ }
+ }
+ }
+ root.add(button2)
+ val button3 = Button("To jest przycisk IMG", image = Img("kotlin.png"))
+ button3.setEventListener {
+ click = { e ->
+ println("3" + e)
+ button.setEventListener<Button> {
+ click = { _ -> println(self.text) }
+ dblclick = { e -> println("111" + e) }
+ }
+ }
+ }
+ root.add(button3)
+
+ println("init routing")
+ routing.on({ -> println("root") })
+ .on("/abc", { -> println("abc") })
+ .on("/test", { -> println("test") })
+ .resolve()
+
+// routing.on(RegExp("/abc/def/(.*)/(.*)/(.*)"), { x,y,z,u,v -> println(x) })
+
+// router.on("/test", { -> println("test") })
+
+/* val vnode = h("div", snOpt {
+ on = snEvents {
+ click = { e -> println(e) }
+ }
+ }, arrayOf(
+ h("span", snOpt {
+ style = snStyle("fontWeight" to "bold", "fontStyle" to "italic")
+ }, arrayOf("This is bold")),
+ " and this is just normal text ",
+ h("a", snOpt {
+ props = snProps("href" to "/foo", "target" to "_blank")
+ }, "I\'ll take you places!")
+ ))
+ val v = patch(container, vnode)
+ val vnode2 = virtualize("<a href='/top' target='_top'>Test2</a>")
+ patch(v, vnode2)*/
+ }
+
+ override fun dispose(): Map<String, Any> {
+ KVManager.shutdown()
+ return mapOf<String, Any>()
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/basic/Label.kt b/src/main/kotlin/pl/treksoft/kvision/basic/Label.kt
new file mode 100644
index 00000000..c0aa5ad2
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/basic/Label.kt
@@ -0,0 +1,7 @@
+package pl.treksoft.kvision.basic
+
+import pl.treksoft.kvision.html.InTag
+import pl.treksoft.kvision.html.INTAG
+
+open class Label(text: String, rich: Boolean = false) : InTag(INTAG.SPAN, text, rich) {
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Container.kt b/src/main/kotlin/pl/treksoft/kvision/core/Container.kt
new file mode 100644
index 00000000..575fa73b
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/core/Container.kt
@@ -0,0 +1,32 @@
+package pl.treksoft.kvision.core
+
+import com.github.snabbdom.VNode
+
+open class Container(classes: Set<String> = setOf()) : Widget(classes) {
+ private val children: MutableList<Widget> = mutableListOf()
+
+ override fun render(): VNode {
+ return kvh("div", childrenVNodes())
+ }
+
+ protected open fun childrenVNodes(): Array<VNode> {
+ return children.filter { it.visible }.map { it.render() }.toTypedArray()
+ }
+
+ open fun add(child: Widget) {
+ children.add(child)
+ child.parent = this
+ refresh()
+ }
+
+ open fun remove(child: Widget) {
+ children.remove(child)
+ child.clearParent()
+ refresh()
+ }
+
+ open fun removeAt(index: Int) {
+ children.removeAt(index).clearParent()
+ refresh()
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Img.kt b/src/main/kotlin/pl/treksoft/kvision/core/Img.kt
new file mode 100644
index 00000000..08063a4a
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/core/Img.kt
@@ -0,0 +1,10 @@
+package pl.treksoft.kvision.core
+
+typealias ResString = String
+
+object Img {
+ operator fun invoke(src: ResString) = at(src)
+
+ @Suppress("UnsafeCastFromDynamic", "NOTHING_TO_INLINE")
+ private inline fun at(src: ResString): String = pl.treksoft.kvision.require("./img/" + src)
+}
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/KVManager.kt b/src/main/kotlin/pl/treksoft/kvision/core/KVManager.kt
new file mode 100644
index 00000000..97f0fd62
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/core/KVManager.kt
@@ -0,0 +1,34 @@
+package pl.treksoft.kvision.core
+
+import com.github.snabbdom.*
+import pl.treksoft.kvision.require
+import pl.treksoft.kvision.routing.routing
+import kotlin.browser.document
+import kotlin.dom.clear
+
+object KVManager {
+ private val bootstrap_webpack = require("bootstrap-webpack")
+ private val font_awesome_webpack = require("font-awesome-webpack")
+
+ private val sdPatch = Snabbdom.init(arrayOf(classModule, attributesModule, propsModule, styleModule, eventListenersModule, datasetModule))
+ private val sdVirtualize = require("snabbdom-virtualize/strings").default
+
+ internal fun patch(id: String, vnode: VNode): VNode {
+ val container = document.getElementById(id)
+ container?.clear()
+ return sdPatch(container, vnode)
+ }
+
+ internal fun patch(oldVNode: VNode, newVNode: VNode): VNode {
+ return sdPatch(oldVNode, newVNode)
+ }
+
+ @Suppress("UnsafeCastFromDynamic")
+ internal fun virtualize(html: String): VNode {
+ return sdVirtualize(html)
+ }
+
+ fun shutdown() {
+ routing.destroy()
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/KVObject.kt b/src/main/kotlin/pl/treksoft/kvision/core/KVObject.kt
new file mode 100644
index 00000000..aa90ccf8
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/core/KVObject.kt
@@ -0,0 +1,3 @@
+package pl.treksoft.kvision.core
+
+interface KVObject
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Root.kt b/src/main/kotlin/pl/treksoft/kvision/core/Root.kt
new file mode 100644
index 00000000..b5911c36
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/core/Root.kt
@@ -0,0 +1,27 @@
+package pl.treksoft.kvision.core
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.snabbdom.StringBoolPair
+
+class Root(id: String, private val fluid: Boolean = false) : Container() {
+ private var vnode: VNode = render()
+
+ init {
+ vnode = KVManager.patch(id, this.render())
+ this.id = id
+ }
+
+ override fun render(): VNode {
+ return kvh("div#" + id, childrenVNodes())
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val css = if (fluid) "container-fluid" else "container"
+ return super.getSnClass() + (css to true)
+ }
+
+ override fun refresh() {
+ vnode = KVManager.patch(vnode, render())
+ }
+
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt
new file mode 100644
index 00000000..8da8b0b6
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt
@@ -0,0 +1,121 @@
+package pl.treksoft.kvision.core
+
+import com.github.snabbdom.On
+import com.github.snabbdom.VNode
+import com.github.snabbdom.VNodeData
+import com.github.snabbdom.h
+import pl.treksoft.kvision.snabbdom.*
+
+open class Widget(classes: Set<String> = setOf()) : KVObject {
+
+ val classes = classes.toMutableSet()
+ val listeners = mutableListOf<SnOn<Widget>.() -> Unit>()
+
+ var parent: Widget? = null
+ internal set
+
+ var visible: Boolean = true
+ set(value) {
+ field = value
+ refresh()
+ }
+ var title: String? = null
+ set(value) {
+ field = value
+ refresh()
+ }
+ var id: String? = null
+ set(value) {
+ field = value
+ refresh()
+ }
+
+ internal open fun render(): VNode {
+ return kvh("div")
+ }
+
+ protected open fun kvh(s: String): VNode {
+ return h(s, getSnOpt())
+ }
+
+ protected open fun kvh(s: String, children: Array<dynamic>): VNode {
+ return h(s, getSnOpt(), children)
+ }
+
+ protected open fun getSnOpt(): VNodeData {
+ return snOpt {
+ attrs = snAttrs(* getSnAttrs().toTypedArray())
+ style = snStyle(* getSnStyle().toTypedArray())
+ `class` = snClasses(* getSnClass().toTypedArray())
+ on = getSnOn()
+ }
+ }
+
+ protected open fun getSnStyle(): List<StringPair> {
+ return listOf()
+ }
+
+ protected open fun getSnClass(): List<StringBoolPair> {
+ return classes.map { c -> c to true } + if (visible) listOf() else listOf("hidden" to true)
+ }
+
+
+ protected open fun getSnAttrs(): List<StringPair> {
+ val snattrs = mutableListOf<StringPair>()
+ if (id != null) {
+ snattrs.add("id" to id.orEmpty())
+ }
+ if (title != null) {
+ snattrs.add("title" to title.orEmpty())
+ }
+ return snattrs
+ }
+
+ protected open fun getSnOn(): On {
+ val handlers = On(this)
+ listeners.forEach { on -> (handlers::apply)(on) }
+ return handlers
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ open fun <T : Widget> setEventListener(block: SnOn<T>.() -> Unit) {
+ listeners.add(block as SnOn<Widget>.() -> Unit)
+ refresh()
+ }
+
+ open fun setEventListener(block: SnOn<Widget>.() -> Unit) {
+ listeners.add(block)
+ refresh()
+ }
+
+ open fun removeEventListeners() {
+ listeners.clear()
+ refresh()
+ }
+
+ open fun show() {
+ visible = true
+ }
+
+ open fun hide() {
+ visible = false
+ }
+
+ open fun addCssClass(css: String) {
+ this.classes.add(css)
+ refresh()
+ }
+
+ open fun removeCssClass(css: String) {
+ this.classes.remove(css)
+ refresh()
+ }
+
+ internal fun clearParent() {
+ this.parent = null
+ }
+
+ protected open fun refresh() {
+ this.parent?.refresh()
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt b/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt
new file mode 100644
index 00000000..eef229e4
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt
@@ -0,0 +1,55 @@
+package pl.treksoft.kvision.dropdown
+
+import com.github.snabbdom.VNode
+import com.github.snabbdom.h
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.KVManager
+import pl.treksoft.kvision.core.ResString
+import pl.treksoft.kvision.html.*
+import pl.treksoft.kvision.snabbdom.StringPair
+
+open class DropDown(text: String, elements: List<String>, icon: String? = null, style: BUTTON_STYLE = BUTTON_STYLE.DEFAULT, size: BUTTON_SIZE? = null,
+ block: Boolean = false, disabled: Boolean = false, image: ResString? = null, classes: Set<String> = setOf()) : Container(classes) {
+ val idk = "abc"
+ val button: DropDownButton = DropDownButton(idk, text, icon, style, size, block, disabled, image, setOf("dropdown"))
+ val list: DropDownListTag = DropDownListTag(idk, elements, setOf("dropdown-menu"))
+
+ init {
+ this.addCssClass("dropdown")
+ this.add(button)
+ this.add(list)
+ }
+}
+
+open class DropDownButton(id: String, text: String, icon: String? = null, style: BUTTON_STYLE = BUTTON_STYLE.DEFAULT, size: BUTTON_SIZE? = null,
+ block: Boolean = false, disabled: Boolean = false, image: ResString? = null, classes: Set<String> = setOf()) :
+ Button(text, icon, style, size, block, disabled, image, classes) {
+
+ init {
+ this.id = id
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ return super.getSnAttrs() + listOf("data-toggle" to "dropdown", "aria-haspopup" to "true", "aria-expanded" to "false")
+ }
+}
+
+open class DropDownListTag(val ariaId: String, elements: List<String>, classes: Set<String> = setOf()) : ListTag(LIST.UL, elements, true, classes) {
+
+ override fun render(): VNode {
+ val children = elements.map { el -> element("li", el, true) }.toTypedArray()
+ return kvh(type.tagName, children)
+ }
+
+ private fun element(name: String, value: String, rich: Boolean): VNode {
+ if (rich) {
+ return h(name, arrayOf(KVManager.virtualize("<a href='#'>$value</a>")))
+ } else {
+ return h(name, value)
+ }
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ return super.getSnAttrs() + listOf("aria-labelledby" to ariaId)
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/Button.kt b/src/main/kotlin/pl/treksoft/kvision/html/Button.kt
new file mode 100644
index 00000000..81df4eaf
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/html/Button.kt
@@ -0,0 +1,99 @@
+package pl.treksoft.kvision.html
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.core.KVManager
+import pl.treksoft.kvision.core.ResString
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.snabbdom.StringBoolPair
+import pl.treksoft.kvision.snabbdom.StringPair
+
+enum class BUTTON_STYLE(val className: String) {
+ DEFAULT("btn-default"),
+ PRIMARY("btn-primary"),
+ SUCCESS("btn-success"),
+ INFO("btn-info"),
+ WARNING("btn-warning"),
+ DANGER("btn-danger"),
+ LINK("btn-link")
+}
+
+enum class BUTTON_SIZE(val className: String) {
+ LARGE("btn-lg"),
+ SMALL("btn-sm"),
+ XSMALL("btn-xs")
+}
+
+open class Button(text: String, icon: String? = null, style: BUTTON_STYLE = BUTTON_STYLE.DEFAULT, size: BUTTON_SIZE? = null,
+ block: Boolean = false, disabled: Boolean = false, image: ResString? = null, classes: Set<String> = setOf()) : Widget(classes) {
+ var text = text
+ set(value) {
+ field = value
+ refresh()
+ }
+ var icon = icon
+ set(value) {
+ field = value
+ refresh()
+ }
+ var style = style
+ set(value) {
+ field = value
+ refresh()
+ }
+ var size = size
+ set(value) {
+ field = value
+ refresh()
+ }
+ var block = block
+ set(value) {
+ field = value
+ refresh()
+ }
+ var disabled = disabled
+ set(value) {
+ field = value
+ refresh()
+ }
+ var image = image
+ set(value) {
+ field = value
+ refresh()
+ }
+
+ override fun render(): VNode {
+ val t = if (icon != null) {
+ if (icon?.startsWith("fa-") == true) {
+ arrayOf(KVManager.virtualize("<i class='fa $icon fa-lg'></i>"), " " + text)
+ } else {
+ arrayOf(KVManager.virtualize("<span class='glyphicon glyphicon-$icon'></span>"), " " + text)
+ }
+ } else if (image != null) {
+ arrayOf(KVManager.virtualize("<img src='$image' alt='' />"), " " + text)
+ } else {
+ arrayOf(text)
+ }
+ return kvh("button", t)
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("btn" to true)
+ cl.add(style.className to true)
+ if (size != null) {
+ cl.add(size?.className.orEmpty() to true)
+ }
+ if (block) {
+ cl.add("btn-block" to true)
+ }
+ if (disabled) {
+ cl.add("disabled" to true)
+ }
+ return cl
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ return super.getSnAttrs() + ("type" to "button")
+ }
+
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/Image.kt b/src/main/kotlin/pl/treksoft/kvision/html/Image.kt
new file mode 100644
index 00000000..3e9fe5d6
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/html/Image.kt
@@ -0,0 +1,66 @@
+package pl.treksoft.kvision.html
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.core.ResString
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.snabbdom.StringBoolPair
+import pl.treksoft.kvision.snabbdom.StringPair
+
+enum class IMAGE_SHAPE(val className: String) {
+ ROUNDED("img-rounded"),
+ CIRCLE("img-circle"),
+ THUMBNAIL("img-thumbnail")
+}
+
+open class Image(src: ResString, alt: String? = null, responsive: Boolean = false, shape: IMAGE_SHAPE? = null, centered: Boolean = false, classes: Set<String> = setOf()) : Widget(classes) {
+ var src = src
+ set(value) {
+ field = value
+ refresh()
+ }
+ var alt = alt
+ set(value) {
+ field = value
+ refresh()
+ }
+ var responsive = responsive
+ set(value) {
+ field = value
+ refresh()
+ }
+ var shape = shape
+ set(value) {
+ field = value
+ refresh()
+ }
+ var centered = centered
+ set(value) {
+ field = value
+ refresh()
+ }
+
+ override fun render(): VNode {
+ return kvh("img")
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ val pr = super.getSnAttrs().toMutableList()
+ pr.add("src" to src)
+ if (alt != null) pr.add("alt" to alt.orEmpty())
+ return pr
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ if (responsive) {
+ cl.add("img-responsive" to true)
+ }
+ if (centered) {
+ cl.add("center-block" to true)
+ }
+ if (shape != null) {
+ cl.add(shape?.className.orEmpty() to true)
+ }
+ return cl
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/InTag.kt b/src/main/kotlin/pl/treksoft/kvision/html/InTag.kt
new file mode 100644
index 00000000..88f69435
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/html/InTag.kt
@@ -0,0 +1,48 @@
+package pl.treksoft.kvision.html
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.core.KVManager
+import pl.treksoft.kvision.core.Widget
+
+enum class INTAG(val tagName: String) {
+ MARK("mark"),
+ DEL("del"),
+ S("s"),
+ INS("ins"),
+ U("u"),
+ SMALL("small"),
+ STRONG("strong"),
+ EM("em"),
+ CITE("cite"),
+ CODE("code"),
+ KBD("kbd"),
+ VAR("var"),
+ SAMP("samp"),
+ SPAN("span")
+}
+
+open class InTag(type: INTAG, text: String, rich: Boolean = false, classes: Set<String> = setOf()) : Widget(classes) {
+ var type = type
+ set(value) {
+ field = value
+ refresh()
+ }
+ var text = text
+ set(value) {
+ field = value
+ refresh()
+ }
+ var rich = rich
+ set(value) {
+ field = value
+ refresh()
+ }
+
+ override fun render(): VNode {
+ if (rich) {
+ return kvh(type.tagName, arrayOf(KVManager.virtualize("<span>$text</span>")))
+ } else {
+ return kvh(type.tagName, arrayOf(text))
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/List.kt b/src/main/kotlin/pl/treksoft/kvision/html/List.kt
new file mode 100644
index 00000000..918f1576
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/html/List.kt
@@ -0,0 +1,62 @@
+package pl.treksoft.kvision.html
+
+import com.github.snabbdom.VNode
+import com.github.snabbdom.h
+import pl.treksoft.kvision.core.KVManager
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.snabbdom.StringBoolPair
+
+enum class LIST(val tagName: String) {
+ UL("ul"),
+ OL("ol"),
+ UNSTYLED("ul"),
+ INLINE("ul"),
+ DL("dl"),
+ DL_HORIZ("dl")
+}
+
+open class ListTag(type: LIST, elements: List<String>, rich: Boolean = false, classes: Set<String> = setOf()) : Widget(classes) {
+ var type = type
+ set(value) {
+ field = value
+ refresh()
+ }
+ var elements = elements
+ set(value) {
+ field = value
+ refresh()
+ }
+ var rich = rich
+ set(value) {
+ field = value
+ refresh()
+ }
+
+ override fun render(): VNode {
+ val children = when (type) {
+ LIST.UL, LIST.OL, LIST.UNSTYLED, LIST.INLINE -> elements.map { el -> element("li", el, rich) }
+ LIST.DL, LIST.DL_HORIZ -> elements.mapIndexed { index, el -> element(if (index % 2 == 0) "dt" else "dd", el, rich) }
+ }.toTypedArray()
+ return kvh(type.tagName, children)
+ }
+
+ private fun element(name: String, value: String, rich: Boolean): VNode {
+ if (rich) {
+ return h(name, arrayOf(KVManager.virtualize("<span>$value</span>")))
+ } else {
+ return h(name, value)
+ }
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ if (type == LIST.UNSTYLED) {
+ cl.add("list-unstyled" to true)
+ } else if (type == LIST.INLINE) {
+ cl.add("list-inline" to true)
+ } else if (type == LIST.DL_HORIZ) {
+ cl.add("dl-horizontal" to true)
+ }
+ return cl
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt b/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt
new file mode 100644
index 00000000..60910707
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt
@@ -0,0 +1,69 @@
+package pl.treksoft.kvision.html
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.core.KVManager
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.snabbdom.StringBoolPair
+
+enum class TAG(val tagName: String) {
+ H1("h1"),
+ H2("h2"),
+ H3("h3"),
+ H4("h4"),
+ H5("h5"),
+ H6("h6"),
+ P("p"),
+ ABBR("abbr"),
+ ADDRESS("address"),
+ BLOCKQUOTE("blockquote"),
+ FOOTER("footer"),
+ PRE("pre")
+}
+
+enum class ALIGN(val className: String) {
+ NONE(""),
+ LEFT("text-left"),
+ CENTER("text-center"),
+ RIGHT("text-right"),
+ JUSTIFY("text-justify"),
+ NOWRAP("text-nowrap")
+}
+
+open class Tag(type: TAG, text: String, rich: Boolean = false, align: ALIGN = ALIGN.NONE, classes: Set<String> = setOf()) : Widget(classes) {
+ var type = type
+ set(value) {
+ field = value
+ refresh()
+ }
+ var text = text
+ set(value) {
+ field = value
+ refresh()
+ }
+ var rich = rich
+ set(value) {
+ field = value
+ refresh()
+ }
+ var align = align
+ set(value) {
+ field = value
+ refresh()
+ }
+
+ override fun render(): VNode {
+ if (rich) {
+ return kvh(type.tagName, arrayOf(KVManager.virtualize("<span>$text</span>")))
+ } else {
+ return kvh(type.tagName, arrayOf(text))
+ }
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ if (align != ALIGN.NONE) {
+ cl.add(align.className to true)
+ }
+ return cl
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/pl/treksoft/kvision/routing/Routing.kt b/src/main/kotlin/pl/treksoft/kvision/routing/Routing.kt
new file mode 100644
index 00000000..709f966c
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/routing/Routing.kt
@@ -0,0 +1,7 @@
+package pl.treksoft.kvision.routing
+
+import pl.treksoft.navigo.Navigo
+
+open class Routing : Navigo(null, true, "#!")
+
+val routing = Routing()
diff --git a/src/main/kotlin/pl/treksoft/kvision/snabbdom/Types.kt b/src/main/kotlin/pl/treksoft/kvision/snabbdom/Types.kt
new file mode 100644
index 00000000..789417e6
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/snabbdom/Types.kt
@@ -0,0 +1,57 @@
+package pl.treksoft.kvision.snabbdom
+
+import com.github.snabbdom.*
+import pl.treksoft.kvision.core.Widget
+
+external class Object
+
+fun obj(init: dynamic.() -> Unit): dynamic {
+ return (Object()).apply(init)
+}
+
+@Suppress("UnsafeCastFromDynamic")
+private fun VNodeData(): VNodeData = js("({})")
+
+interface SnOn<T> : On {
+ var self: T
+}
+
+fun snOpt(block: VNodeData.() -> Unit) = (VNodeData()::apply)(block)
+
+@Suppress("UnsafeCastFromDynamic")
+internal fun On(widget: Widget): SnOn<Widget> {
+ val obj = js("({})")
+ obj["self"] = widget
+ return obj
+}
+
+typealias StringPair = Pair<String, String>
+typealias StringBoolPair = Pair<String, Boolean>
+
+@Suppress("UnsafeCastFromDynamic")
+fun snStyle(vararg pairs: StringPair): VNodeStyle {
+ return obj {
+ pairs.forEach { (key, value) -> this[key] = value }
+ }
+}
+
+@Suppress("UnsafeCastFromDynamic")
+fun snProps(vararg pairs: StringPair): Props {
+ return obj {
+ pairs.forEach { (key, value) -> this[key] = value }
+ }
+}
+
+@Suppress("UnsafeCastFromDynamic")
+fun snClasses(vararg pairs: StringBoolPair): Classes {
+ return obj {
+ pairs.forEach { (key, value) -> this[key] = value }
+ }
+}
+
+@Suppress("UnsafeCastFromDynamic")
+fun snAttrs(vararg pairs: StringPair): Attrs {
+ return obj {
+ pairs.forEach { (key, value) -> this[key] = value }
+ }
+}
diff --git a/src/main/web/index.html b/src/main/web/index.html
index 254831ca..4dfaccf4 100644
--- a/src/main/web/index.html
+++ b/src/main/web/index.html
@@ -1,9 +1,19 @@
<!DOCTYPE html>
<html>
<head>
- <title>KVision</title>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>KVision Showcase</title>
<script type="text/javascript" src="main.bundle.js"></script>
+ <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
+ <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+ <!--[if lt IE 9]>
+ <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
+ <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
+ <![endif]-->
</head>
-<body class="kvision">
+<body>
+<div id="showcase"></div>
</body>
</html>
diff --git a/src/test/kotlin/test/TestMainApplication.kt b/src/test/kotlin/test/TestMainApplication.kt
deleted file mode 100644
index 0f8824f7..00000000
--- a/src/test/kotlin/test/TestMainApplication.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package test
-
-import org.junit.*
-import kotlin.test.*
-import pl.treksoft.kvision.*
-
-class TestMainApplication {
-} \ No newline at end of file
diff --git a/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt b/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt
new file mode 100644
index 00000000..a1a142e7
--- /dev/null
+++ b/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt
@@ -0,0 +1,48 @@
+package test.pl.treksoft.kvision
+
+import org.w3c.dom.Element
+import org.w3c.dom.HTMLElement
+import pl.treksoft.kvision.core.Root
+import pl.treksoft.kvision.core.Widget
+import kotlin.browser.document
+
+interface TestSpec {
+ fun beforeTest()
+
+ fun afterTest()
+
+ fun run(code: () -> Unit) {
+ beforeTest()
+ code()
+ afterTest()
+ }
+}
+
+interface DomSpec : TestSpec {
+
+ override fun beforeTest() {
+ var fixture = "<div style=\"display: none\"><div id=\"test\"></div></div>";
+ document.body?.insertAdjacentHTML("afterbegin", fixture);
+ }
+
+ override fun afterTest() {
+ val div = document.getElementById("test")
+ div?.remove()
+ }
+
+}
+
+interface WSpec : DomSpec {
+
+ fun runW(code: (widget: Widget, element: Element?) -> Unit) {
+ beforeTest()
+ val root = Root("test")
+ val widget = Widget()
+ widget.id = "test_id"
+ root.add(widget)
+ val element = document.getElementById("test_id")
+ code(widget, element)
+ afterTest()
+ }
+
+} \ No newline at end of file
diff --git a/src/test/kotlin/test/pl/treksoft/kvision/core/ContainerSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/core/ContainerSpec.kt
new file mode 100644
index 00000000..f79d24df
--- /dev/null
+++ b/src/test/kotlin/test/pl/treksoft/kvision/core/ContainerSpec.kt
@@ -0,0 +1,68 @@
+package test.pl.treksoft.kvision.core
+
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.Root
+import pl.treksoft.kvision.core.Widget
+import test.pl.treksoft.kvision.DomSpec
+import kotlin.browser.document
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class ContainerSpec : DomSpec {
+
+ @Test
+ fun add() {
+ run {
+ val root = Root("test")
+ val container = Container()
+ val child1 = Widget()
+ child1.id = "child1"
+ val child2 = Widget()
+ child2.id = "child2"
+ container.add(child1)
+ container.add(child2)
+ root.add(container)
+ val elem1 = document.getElementById("child1")
+ val elem2 = document.getElementById("child2")
+ assertTrue("Container renders children") { elem1 != null && elem2 != null }
+ }
+ }
+
+ @Test
+ fun remove() {
+ run {
+ val root = Root("test")
+ val container = Container()
+ val child1 = Widget()
+ child1.id = "child1"
+ val child2 = Widget()
+ child2.id = "child2"
+ container.add(child1)
+ container.add(child2)
+ root.add(container)
+ container.remove(child2)
+ val elem1 = document.getElementById("child1")
+ val elem2 = document.getElementById("child2")
+ assertTrue("Container renders children") { elem1 != null && elem2 == null }
+ }
+ }
+
+ @Test
+ fun removeAt() {
+ run {
+ val root = Root("test")
+ val container = Container()
+ val child1 = Widget()
+ child1.id = "child1"
+ val child2 = Widget()
+ child2.id = "child2"
+ container.add(child1)
+ container.add(child2)
+ root.add(container)
+ container.removeAt(0)
+ val elem1 = document.getElementById("child1")
+ val elem2 = document.getElementById("child2")
+ assertTrue("Container renders children") { elem1 == null && elem2 != null }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/test/kotlin/test/pl/treksoft/kvision/core/KVManagerSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/core/KVManagerSpec.kt
new file mode 100644
index 00000000..d123cf310
--- /dev/null
+++ b/src/test/kotlin/test/pl/treksoft/kvision/core/KVManagerSpec.kt
@@ -0,0 +1,58 @@
+package test.pl.treksoft.kvision.core
+
+import com.github.snabbdom.h
+import pl.treksoft.kvision.core.KVManager
+import pl.treksoft.kvision.snabbdom.snAttrs
+import pl.treksoft.kvision.snabbdom.snOpt
+import pl.treksoft.kvision.snabbdom.snStyle
+import test.pl.treksoft.kvision.DomSpec
+import kotlin.browser.document
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class KVManagerSpec : DomSpec {
+
+ @Test
+ fun patch_ById() {
+ run {
+ val vnode = h("span", snOpt {
+ attrs = snAttrs("id" to "test_new")
+ style = snStyle("fontWeight" to "bold", "fontStyle" to "italic")
+ })
+ KVManager.patch("test", vnode)
+ assertTrue("Original container should not exist") { document.getElementById("test") == null }
+ assertTrue("New container should exist") { document.getElementById("test_new") != null }
+ }
+ }
+
+ @Test
+ fun patch_ByVnode() {
+ run {
+ val vnode1 = h("span", snOpt {
+ attrs = snAttrs("id" to "test2")
+ style = snStyle("fontWeight" to "bold", "fontStyle" to "italic")
+ })
+ val vnode2 = KVManager.patch("test", vnode1)
+ val vnode3 = h("span", snOpt {
+ attrs = snAttrs("id" to "test3")
+ style = snStyle("fontWeight" to "bold", "fontStyle" to "italic")
+ })
+ KVManager.patch(vnode2, vnode3)
+ assertTrue("First container should not exist") { document.getElementById("test") == null }
+ assertTrue("Second container should not exist") { document.getElementById("test2") == null }
+ assertTrue("Third container should exist") { document.getElementById("test3") != null }
+ }
+ }
+
+ @Test
+ fun virtualize() {
+ run {
+ val node = KVManager.virtualize("<div id=\"virtual\"><p>Virtual node</p></div>")
+ KVManager.patch("test", node)
+ assertTrue("Original container should not exist") { document.getElementById("test") == null }
+ val v = document.getElementById("virtual")
+ assertTrue("New container should exist") { v != null }
+ assertTrue("New container should have one child") { v?.children?.length == 1 }
+ }
+ }
+}
diff --git a/src/test/kotlin/test/pl/treksoft/kvision/core/RootSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/core/RootSpec.kt
new file mode 100644
index 00000000..b7435e73
--- /dev/null
+++ b/src/test/kotlin/test/pl/treksoft/kvision/core/RootSpec.kt
@@ -0,0 +1,29 @@
+package test.pl.treksoft.kvision.core
+
+import pl.treksoft.kvision.core.Root
+import test.pl.treksoft.kvision.DomSpec
+import kotlin.browser.document
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class RootSpec : DomSpec {
+
+ @Test
+ fun getSnClass() {
+ run {
+ val root = Root("test")
+ val rootElem = document.getElementById("test")
+ assertTrue("Standard root container has correct css class") { rootElem?.className == "container" }
+ }
+ }
+
+ @Test
+ fun getSnClass_Fluid() {
+ run {
+ val root = Root("test", fluid = true)
+ val rootElem = document.getElementById("test")
+ assertTrue("Fluid root container has correct css class") { rootElem?.className == "container-fluid" }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/test/kotlin/test/pl/treksoft/kvision/core/WidgetSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/core/WidgetSpec.kt
new file mode 100644
index 00000000..214ee1f5
--- /dev/null
+++ b/src/test/kotlin/test/pl/treksoft/kvision/core/WidgetSpec.kt
@@ -0,0 +1,89 @@
+package test.pl.treksoft.kvision.core
+
+import test.pl.treksoft.kvision.WSpec
+import kotlin.browser.document
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class WidgetSpec : WSpec {
+
+ @Test
+ fun render() {
+ runW { widget, element ->
+ widget.addCssClass("testClass")
+ widget.title = "test_title"
+ val vnode = widget.render()
+ assertTrue("VNode has correct class attribute") { vnode.data.`class`.testClass == true }
+ assertTrue("VNode has correct id attribute") { vnode.data.attrs.id == "test_id" }
+ assertTrue("VNode has correct title attribute") { vnode.data.attrs.title == "test_title" }
+ }
+ }
+
+ @Test
+ fun render_ToDom() {
+ runW { widget, element ->
+ widget.addCssClass("testClass")
+ widget.title = "test_title"
+ assertTrue("New element should be created") { element != null }
+ assertTrue("New element should have correct css class") { element?.className == "testClass" }
+ assertTrue("New element should have correct id attribute") { element?.id == "test_id" }
+ assertTrue("New element should have correct title attribute") { element?.getAttribute("title") == "test_title" }
+ }
+ }
+
+ @Test
+ fun hide() {
+ runW { widget, element ->
+ widget.hide()
+ val element = document.getElementById("test_id")
+ assertTrue("Hidden element should not be accessible") { element == null }
+ }
+ }
+
+ @Test
+ fun show() {
+ runW { widget, element ->
+ widget.hide()
+ widget.show()
+ val element = document.getElementById("test_id")
+ assertTrue("Visible element should be accessible") { element != null }
+ }
+ }
+
+ @Test
+ fun addCssClass() {
+ runW { widget, element ->
+ widget.addCssClass("test_class1")
+ widget.addCssClass("test_class2")
+ assertTrue("Element should have correct css classes") { element?.className == "test_class1 test_class2" }
+ }
+ }
+
+ @Test
+ fun removeCssClass() {
+ runW { widget, element ->
+ widget.addCssClass("test_class1")
+ widget.addCssClass("test_class2")
+ widget.removeCssClass("test_class1")
+ assertTrue("Element should have correct css class") { element?.className == "test_class2" }
+ }
+ }
+
+ @Test
+ fun setEventListener() {
+ runW { widget, element ->
+ widget.setEventListener { click = { _ -> } }
+ assertTrue("Element should have an event listener") { widget.listeners.size == 1 }
+ }
+ }
+
+ @Test
+ fun removeEventListener() {
+ runW { widget, element ->
+ widget.setEventListener { click = { _ -> } }
+ widget.removeEventListeners()
+ assertTrue("Element should not have any event listener") { widget.listeners.size == 0 }
+ }
+ }
+
+} \ No newline at end of file