diff options
30 files changed, 1146 insertions, 47 deletions
diff --git a/build.gradle b/build.gradle index fc9e4a1f..c9cf1563 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.1.4' + ext.kotlin_version = '1.1.4-3' repositories { jcenter() @@ -21,18 +21,34 @@ apply plugin: 'org.jetbrains.kotlin.frontend' repositories { jcenter() maven { url = 'https://dl.bintray.com/gbaldeck/kotlin' } + maven { url = 'https://dl.bintray.com/rjaros/kotlin' } } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" // for now only compile configuration is supported compile "com.github.snabbdom:snabbdom-kotlin:0.1.0" + compile "pl.treksoft:navigo-kotlin:0.0.1" + } kotlinFrontend { npm { + dependency "jquery" + dependency "bootstrap" + dependency "css-loader" dependency "style-loader" - dependency ("snabbdom", "0.7.0") + dependency "less" + dependency "less-loader" + dependency "imports-loader" + dependency "bootstrap-webpack" + dependency "font-awesome" + dependency("font-awesome-webpack", "0.0.5-beta.2") + dependency "file-loader" + dependency "url-loader" + dependency("snabbdom", "0.6.9") + dependency "snabbdom-virtualize" + dependency "navigo" devDependency("karma") } @@ -43,17 +59,6 @@ kotlinFrontend { define "PRODUCTION", false -// rollupBundle { -// bundleName = "rolledUp" -// } - -// allBundles { -// /* set properties for all bundles */ -// } - -// bundle("someBundler") { -// .... -// } } compileKotlin2Js { @@ -61,7 +66,6 @@ compileKotlin2Js { kotlinOptions.outputFile = "$project.buildDir.path/js/${project.name}.js" kotlinOptions.sourceMap = true kotlinOptions.moduleKind = 'commonjs' - kotlinOptions.main = "call" } compileTestKotlin2Js { @@ -69,6 +73,14 @@ compileTestKotlin2Js { kotlinOptions.outputFile = "$project.buildDir.path/js-tests/${project.name}-tests.js" kotlinOptions.sourceMap = true kotlinOptions.moduleKind = 'commonjs' -// kotlinOptions.moduleName = project.name + "-test" - kotlinOptions.main = "call" +} + +task copyResources(type: Copy) { + from "src/main/assets" + into file(buildDir.path + "/js") +} + +afterEvaluate { + tasks.getByName("webpack-bundle") { dependsOn(copyResources) } + tasks.getByName("webpack-run") { dependsOn(copyResources) } } diff --git a/src/main/assets/img/kotlin.png b/src/main/assets/img/kotlin.png Binary files differnew file mode 100644 index 00000000..4ac96a24 --- /dev/null +++ b/src/main/assets/img/kotlin.png 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 diff --git a/webpack.config.d/bootstrap.js b/webpack.config.d/bootstrap.js new file mode 100644 index 00000000..32a7c4d0 --- /dev/null +++ b/webpack.config.d/bootstrap.js @@ -0,0 +1,4 @@ +config.module.rules.push({test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/font-woff'}); +config.module.rules.push({test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=application/octet-stream'}); +config.module.rules.push({test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader'}); +config.module.rules.push({test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml'}); diff --git a/webpack.config.d/file.js b/webpack.config.d/file.js new file mode 100644 index 00000000..8b853e7e --- /dev/null +++ b/webpack.config.d/file.js @@ -0,0 +1,6 @@ +config.module.rules.push( + { + test: /\.(jpe?g|png|gif|svg)$/i, + loader: 'file-loader' + } +);
\ No newline at end of file |