aboutsummaryrefslogtreecommitdiff
path: root/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft
diff options
context:
space:
mode:
authorRobert Jaros <rjaros@finn.pl>2019-10-03 19:03:21 +0200
committerRobert Jaros <rjaros@finn.pl>2019-10-03 19:03:21 +0200
commit6b53324c97bfc80ed14dfca6a5dbc879950715b9 (patch)
tree55c7bb7d06e37470795a93e1f542e51ef3f1ace6 /kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft
parent22a8d5c35db97d65a90b21d97e6835380191845d (diff)
downloadkvision-6b53324c97bfc80ed14dfca6a5dbc879950715b9.tar.gz
kvision-6b53324c97bfc80ed14dfca6a5dbc879950715b9.tar.bz2
kvision-6b53324c97bfc80ed14dfca6a5dbc879950715b9.zip
Upgrade to Bootstrap 4.
Upgrade to Font Awesome 5. Restructure modules.
Diffstat (limited to 'kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft')
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/KVManagerBootstrap.kt55
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/core/Component.kt133
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt112
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt417
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt61
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt57
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Alert.kt121
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/CloseIcon.kt48
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Confirm.kt176
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Modal.kt292
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/Nav.kt110
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/NavForm.kt72
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/Navbar.kt229
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel/ResponsiveGridPanel.kt185
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel/TabPanel.kt273
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/progress/ProgressBar.kt162
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/progress/ProgressIndicator.kt125
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/toolbar/ButtonGroup.kt109
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/toolbar/Toolbar.kt63
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/MaximizeIcon.kt48
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/MinimizeIcon.kt48
-rw-r--r--kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/Window.kt449
22 files changed, 3320 insertions, 25 deletions
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/KVManagerBootstrap.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/KVManagerBootstrap.kt
index 678ad33e..1a650117 100644
--- a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/KVManagerBootstrap.kt
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/KVManagerBootstrap.kt
@@ -21,38 +21,43 @@
*/
package pl.treksoft.kvision
-import org.w3c.dom.asList
-import kotlin.browser.document
+import pl.treksoft.kvision.core.Component
+import pl.treksoft.kvision.utils.isIE11
+
+internal val kVManagerBootstrapInit = KVManagerBootstrap.init()
/**
* Internal singleton object which initializes and configures KVision Bootstrap module.
*/
-@Suppress("EmptyCatchBlock", "TooGenericExceptionCaught")
internal object KVManagerBootstrap {
- private val links = document.getElementsByTagName("link")
- private val bootstrapWebpack = try {
- val bootswatch = links.asList().find { it.getAttribute("href")?.contains("bootstrap.min.css") ?: false }
- require("bootstrap")
- if (bootswatch != null) {
- if (bootswatch.getAttribute("href")?.contains("/paper/") == true) {
- require("./css/paper.css")
+ init {
+ require("bootstrap/dist/js/bootstrap.bundle.min.js")
+ require("awesome-bootstrap-checkbox")
+ require("bootstrap-vertical-tabs")
+ require("./css/kvbootstrap.css")
+ }
+
+ private val elementResizeEvent = require("element-resize-event")
+
+ @Suppress("UnsafeCastFromDynamic")
+ internal fun setResizeEvent(component: Component, callback: () -> Unit) {
+ if (!isIE11()) {
+ component.getElement()?.let {
+ elementResizeEvent(it, callback)
}
- } else {
- require("bootstrap/dist/css/bootstrap.min.css")
}
- require("./css/style.css")
- } catch (e: Throwable) {
- }
- private val fontAwesomeWebpack = try {
- require("font-awesome-webpack-4")
- } catch (e: Throwable) {
}
- private val awesomeBootstrapCheckbox = try {
- require("awesome-bootstrap-checkbox")
- } catch (e: Throwable) {
- }
- private val bootstrapVerticalTabsCss = try {
- require("bootstrap-vertical-tabs")
- } catch (e: Throwable) {
+
+ @Suppress("UnsafeCastFromDynamic")
+ internal fun clearResizeEvent(component: Component) {
+ if (!isIE11()) {
+ if (component.getElement()?.asDynamic()?.__resizeTrigger__?.contentDocument != null) {
+ component.getElement()?.let {
+ elementResizeEvent.unbind(it)
+ }
+ }
+ }
}
+
+ internal fun init() {}
}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/core/Component.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/core/Component.kt
new file mode 100644
index 00000000..c35ee9fb
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/core/Component.kt
@@ -0,0 +1,133 @@
+/*
+ * 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
+
+enum class BsBorder(internal val className: String) {
+ BORDER("border"),
+ BORDERTOP("border-top"),
+ BORDERBOTTOM("border-bottom"),
+ BORDERRIGHT("border-right"),
+ BORDERLEFT("border-left"),
+ BORDER_0("border-0"),
+ BORDERTOP_0("border-top-0"),
+ BORDERBOTTOM_0("border-bottom-0"),
+ BORDERRIGHT_0("border-right-0"),
+ BORDERLEFT_0("border-left-0"),
+ BORDERPRIMARY("border-primary"),
+ BORDERSECONDARY("border-secondary"),
+ BORDERSUCCESS("border-success"),
+ BORDERDANGER("border-danger"),
+ BORDERWARNING("border-warning"),
+ BORDERINFO("border-info"),
+ BORDERLIGHT("border-light"),
+ BORDERDARK("border-dark"),
+ BORDERWHITE("border-white")
+}
+
+fun Component.addBsBorder(vararg bsBorder: BsBorder) {
+ bsBorder.forEach {
+ this.addCssClass(it.className)
+ }
+}
+
+fun Component.removeBsBorder(vararg bsBorder: BsBorder) {
+ bsBorder.forEach {
+ this.removeCssClass(it.className)
+ }
+}
+
+enum class BsRounded(internal val className: String) {
+ ROUNDED("rounded"),
+ ROUNDEDTOP("rounded-top"),
+ ROUNDEDBOTTOM("rounded-bottom"),
+ ROUNDEDLEFT("rounded-left"),
+ ROUNDEDRIGHT("rounded-right"),
+ ROUNDEDCIRCLE("rounded-circle"),
+ ROUNDEDPILL("rounded-pill"),
+ ROUNDEDLG("rounded-lg"),
+ ROUNDEDSM("rounded-sm")
+}
+
+fun Component.addBsRounded(vararg bsRounded: BsRounded) {
+ bsRounded.forEach {
+ this.addCssClass(it.className)
+ }
+}
+
+fun Component.removeBsRounded(vararg bsRounded: BsRounded) {
+ bsRounded.forEach {
+ this.removeCssClass(it.className)
+ }
+}
+
+fun Component.addBsClearfix() {
+ this.addCssClass("clearfix")
+}
+
+fun Component.removeBsClearfix() {
+ this.removeCssClass("clearfix")
+}
+
+enum class BsColor(internal val className: String) {
+ PRIMARY("text-primary"),
+ SECONDARY("text-secondary"),
+ SUCCESS("text-success"),
+ DANGER("text-danger"),
+ WARNING("text-warning"),
+ INFO("text-info"),
+ LIGHT("text-light"),
+ DARK("text-dark"),
+ WHITE("text-white"),
+ BODY("text-body"),
+ MUTED("text-muted"),
+ BLACK50("text-black-50"),
+ WHITE50("text-white-50")
+}
+
+fun Component.addBsColor(bsColor: BsColor) {
+ this.addCssClass(bsColor.className)
+}
+
+fun Component.removeBsColor(bsColor: BsColor) {
+ this.removeCssClass(bsColor.className)
+}
+
+enum class BsBgColor(internal val className: String) {
+ PRIMARY("bg-primary"),
+ SECONDARY("bg-secondary"),
+ SUCCESS("bg-success"),
+ DANGER("bg-danger"),
+ WARNING("bg-warning"),
+ INFO("bg-info"),
+ LIGHT("bg-light"),
+ DARK("bg-dark"),
+ WHITE("bg-white"),
+ TRANSPARENT("bg-transparent")
+}
+
+fun Component.addBsBgColor(bsBgColor: BsBgColor) {
+ this.addCssClass(bsBgColor.className)
+}
+
+fun Component.removeBsBgColor(bsBgColor: BsBgColor) {
+ this.removeCssClass(bsBgColor.className)
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt
new file mode 100644
index 00000000..4e20de81
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.dropdown
+
+import org.w3c.dom.events.MouseEvent
+import pl.treksoft.kvision.core.Display
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.html.Div
+import pl.treksoft.kvision.panel.Root
+import pl.treksoft.kvision.utils.px
+
+/**
+ * Context menu component.
+ *
+ * @constructor
+ * @param element an element to bind
+ * @param fixedPosition use fixed positioning
+ * @param classes a set of CSS class names
+ */
+open class ContextMenu(
+ element: Widget? = null,
+ protected val fixedPosition: Boolean = false,
+ classes: Set<String> = setOf(), init: (ContextMenu.() -> Unit)? = null
+) : Div(classes = classes + "dropdown-menu") {
+
+ init {
+ @Suppress("LeakingThis")
+ hide()
+ @Suppress("LeakingThis")
+ display = Display.BLOCK
+ val root = element?.getRoot() ?: Root.getLastRoot()
+ if (root != null) {
+ @Suppress("LeakingThis")
+ root.addContextMenu(this)
+ } else {
+ println("At least one Root object is required to create a context menu!")
+ }
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ /**
+ * Positions and shows a context menu based on a mouse event.
+ * @param mouseEvent mouse event
+ * @return current context menu
+ */
+ open fun positionMenu(mouseEvent: MouseEvent): ContextMenu {
+ if (fixedPosition) {
+ this.top = DEFAULT_FIXED_POS_Y.px
+ this.left = DEFAULT_FIXED_POS_X.px
+ } else {
+ this.top = mouseEvent.pageY.toInt().px
+ this.left = mouseEvent.pageX.toInt().px
+ }
+ this.show()
+ return this
+ }
+
+ companion object {
+
+ const val DEFAULT_FIXED_POS_X = 5
+ const val DEFAULT_FIXED_POS_Y = 5
+
+ /**
+ * Sets context menu for the current widget.
+ * @param contextMenu a context menu
+ * @return current widget
+ */
+ fun Widget.setContextMenu(contextMenu: ContextMenu): Widget {
+ this.setEventListener<Widget> {
+ contextmenu = { e: MouseEvent ->
+ e.preventDefault()
+ contextMenu.positionMenu(e)
+ }
+ }
+ return this
+ }
+
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Widget.contextMenu(
+ fixedPosition: Boolean = false,
+ classes: Set<String> = setOf(), init: (ContextMenu.() -> Unit)? = null
+ ): ContextMenu {
+ val contextMenu = ContextMenu(this, fixedPosition, classes).apply { init?.invoke(this) }
+ this.setContextMenu(contextMenu)
+ return contextMenu
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt
new file mode 100644
index 00000000..9862b322
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt
@@ -0,0 +1,417 @@
+/*
+ * 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.dropdown
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.core.Component
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.CssSize
+import pl.treksoft.kvision.core.ResString
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.StringPair
+import pl.treksoft.kvision.html.Button
+import pl.treksoft.kvision.html.ButtonStyle
+import pl.treksoft.kvision.html.ButtonType
+import pl.treksoft.kvision.html.Div
+import pl.treksoft.kvision.html.Link
+import pl.treksoft.kvision.panel.SimplePanel
+import pl.treksoft.kvision.utils.obj
+
+/**
+ * Useful options for use in DropDown's *elements* parameter.
+ */
+enum class DD(val option: String) {
+ HEADER("DD#HEADER"),
+ DISABLED("DD#DISABLED"),
+ SEPARATOR("DD#SEPARATOR")
+}
+
+/**
+ * Dropdown directions.
+ */
+enum class Direction(internal val direction: String) {
+ DROPDOWN("dropdown"),
+ DROPUP("dropup"),
+ DROPLEFT("dropleft"),
+ DROPRIGHT("dropright")
+}
+
+/**
+ * Bootstrap dropdown component.
+ *
+ * @constructor
+ * @param text the label of the dropdown button
+ * @param elements an optional list of link elements (special options from [DD] enum class can be used as values)
+ * @param icon the icon of the dropdown button
+ * @param style the style of the dropdown button
+ * @param direction the direction of the dropdown
+ * @param disabled determines if the component is disabled on start
+ * @param forNavbar determines if the component will be used in a navbar
+ * @param forDropDown determines if the component will be used in a dropdown
+ * @param classes a set of CSS class names
+ */
+@Suppress("TooManyFunctions")
+open class DropDown(
+ text: String, elements: List<StringPair>? = null, icon: String? = null,
+ style: ButtonStyle = ButtonStyle.PRIMARY, direction: Direction = Direction.DROPDOWN, disabled: Boolean = false,
+ val forNavbar: Boolean = false, val forDropDown: Boolean = false, classes: Set<String> = setOf()
+) : SimplePanel(classes) {
+ /**
+ * Label of the dropdown button.
+ */
+ var text
+ get() = button.text
+ set(value) {
+ button.text = value
+ }
+ private var elements by refreshOnUpdate(elements) { setChildrenFromElements() }
+ /**
+ * The icon of the dropdown button.
+ */
+ var icon
+ get() = button.icon
+ set(value) {
+ button.icon = value
+ }
+ /**
+ * The style of the dropdown button.
+ */
+ var style
+ get() = button.style
+ set(value) {
+ button.style = value
+ }
+ /**
+ * The size of the dropdown button.
+ */
+ var size
+ get() = button.size
+ set(value) {
+ button.size = value
+ }
+ /**
+ * Determines if the dropdown button takes all the space horizontally.
+ */
+ var block
+ get() = button.block
+ set(value) {
+ button.block = value
+ }
+ /**
+ * Determines if the dropdown is disabled.
+ */
+ var disabled
+ get() = button.disabled
+ set(value) {
+ button.disabled = value
+ }
+ /**
+ * The image on the dropdown button.
+ */
+ var image
+ get() = button.image
+ set(value) {
+ button.image = value
+ }
+ /**
+ * The direction of the dropdown.
+ */
+ var direction by refreshOnUpdate(direction)
+ /**
+ * Width of the dropdown button.
+ */
+ override var width: CssSize?
+ get() = super.width
+ set(value) {
+ super.width = value
+ button.width = value
+ }
+
+ private val idc = "kv_dropdown_$counter"
+ internal val button: DropDownButton = DropDownButton(
+ idc, text, icon, style, disabled, forNavbar, forDropDown
+ )
+
+ fun buttonId() = button.id
+
+ internal val list: DropDownDiv = DropDownDiv(idc)
+
+ init {
+ if (forDropDown) {
+ this.style = ButtonStyle.LIGHT
+ this.direction = Direction.DROPRIGHT
+ }
+ setChildrenFromElements()
+ this.addInternal(button)
+ this.addInternal(list)
+ counter++
+ }
+
+ override fun render(): VNode {
+ return if (forNavbar) {
+ render("li", childrenVNodes())
+ } else {
+ render("div", childrenVNodes())
+ }
+ }
+
+ override fun add(child: Component): SimplePanel {
+ list.add(child)
+ return this
+ }
+
+ override fun addAll(children: List<Component>): SimplePanel {
+ list.addAll(children)
+ return this
+ }
+
+ override fun remove(child: Component): SimplePanel {
+ list.remove(child)
+ return this
+ }
+
+ override fun removeAll(): SimplePanel {
+ list.removeAll()
+ return this
+ }
+
+ override fun getChildren(): List<Component> {
+ return list.getChildren()
+ }
+
+ private fun setChildrenFromElements() {
+ list.removeAll()
+ elements?.let { elems ->
+ val c = elems.map {
+ when (it.second) {
+ DD.HEADER.option -> Header(it.first)
+ DD.SEPARATOR.option -> Separator()
+ DD.DISABLED.option -> {
+ Link(it.first, "javascript:void(0)", classes = setOf("dropdown-item", "disabled")).apply {
+ tabindex = -1
+ setAttribute("aria-disabled", "true")
+ }
+ }
+ else -> Link(it.first, it.second, classes = setOf("dropdown-item"))
+ }
+ }
+ list.addAll(c)
+ }
+ }
+
+ @Suppress("UnsafeCastFromDynamic")
+ override fun afterInsert(node: VNode) {
+ this.getElementJQuery()?.on("show.bs.dropdown") { e, _ ->
+ this.dispatchEvent("showBsDropdown", obj { detail = e })
+ }
+ this.getElementJQuery()?.on("shown.bs.dropdown") { e, _ ->
+ this.dispatchEvent("shownBsDropdown", obj { detail = e })
+ }
+ this.getElementJQuery()?.on("hide.bs.dropdown") { e, _ ->
+ this.dispatchEvent("hideBsDropdown", obj { detail = e })
+ }
+ this.getElementJQuery()?.on("hidden.bs.dropdown") { e, _ ->
+ this.dispatchEvent("hiddenBsDropdown", obj { detail = e })
+ }
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ if (forNavbar) cl.add("nav-item" to true)
+ cl.add(direction.direction to true)
+ return cl
+ }
+
+ /**
+ * Toggles dropdown visibility.
+ */
+ open fun toggle() {
+ this.button.getElementJQuery()?.click()
+ }
+
+ companion object {
+ internal var counter = 0
+
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.dropDown(
+ text: String, elements: List<StringPair>? = null, icon: String? = null,
+ style: ButtonStyle = ButtonStyle.PRIMARY, direction: Direction = Direction.DROPDOWN,
+ disabled: Boolean = false, forNavbar: Boolean = false, forDropDown: Boolean = false,
+ classes: Set<String> = setOf(), init: (DropDown.() -> Unit)? = null
+ ): DropDown {
+ val dropDown =
+ DropDown(
+ text,
+ elements,
+ icon,
+ style,
+ direction,
+ disabled,
+ forNavbar,
+ forDropDown,
+ classes
+ ).apply { init?.invoke(this) }
+ this.add(dropDown)
+ return dropDown
+ }
+
+ /**
+ * DSL builder extension function for a link in a dropdown list.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun DropDown.ddLink(
+ label: String, url: String? = null, icon: String? = null, image: ResString? = null,
+ classes: Set<String> = setOf(), init: (Link.() -> Unit)? = null
+ ): Link {
+ val link = Link(label, url, icon, image, classes + "dropdown-item").apply {
+ init?.invoke(this)
+ }
+ this.add(link)
+ return link
+ }
+
+ /**
+ * DSL builder extension function for a link in a context menu list.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun ContextMenu.cmLink(
+ label: String, url: String? = null, icon: String? = null, image: ResString? = null,
+ classes: Set<String> = setOf(), init: (Link.() -> Unit)? = null
+ ): Link {
+ val link = Link(label, url, icon, image, classes + "dropdown-item").apply {
+ init?.invoke(this)
+ }
+ this.add(link)
+ return link
+ }
+
+ /**
+ * DSL builder extension function for a disabled link in a dropdown list.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun DropDown.ddLinkDisabled(
+ label: String, icon: String? = null, image: ResString? = null,
+ classes: Set<String> = setOf(), init: (Link.() -> Unit)? = null
+ ): Link {
+ val link = Link(label, "javascript:void(0)", icon, image, classes + "dropdown-item" + "disabled").apply {
+ tabindex = -1
+ setAttribute("aria-disabled", "true")
+ init?.invoke(this)
+ }
+ this.add(link)
+ return link
+ }
+
+ /**
+ * DSL builder extension function for a disabled link in a context menu list.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun ContextMenu.cmLinkDisabled(
+ label: String, icon: String? = null, image: ResString? = null,
+ classes: Set<String> = setOf(), init: (Link.() -> Unit)? = null
+ ): Link {
+ val link = Link(label, "javascript:void(0)", icon, image, classes + "dropdown-item" + "disabled").apply {
+ tabindex = -1
+ setAttribute("aria-disabled", "true")
+ init?.invoke(this)
+ }
+ this.add(link)
+ return link
+ }
+
+ }
+}
+
+internal class DropDownButton(
+ id: String,
+ text: String,
+ icon: String? = null,
+ style: ButtonStyle = ButtonStyle.PRIMARY,
+ disabled: Boolean = false,
+ val forNavbar: Boolean = false,
+ val forDropDown: Boolean = false,
+ classes: Set<String> = setOf()
+) :
+ Button(text, icon, style, ButtonType.BUTTON, disabled, classes) {
+
+ init {
+ this.id = id
+ if (!forNavbar && !forDropDown) this.role = "button"
+ setInternalEventListener<DropDownButton> {
+ click = { e ->
+ if (parent?.parent is ContextMenu) {
+ e.asDynamic().dropDownCM = true
+ } else if (forDropDown || forNavbar) {
+ (parent as DropDown).list.getElementJQuery()?.toggle()
+ e.stopPropagation()
+ }
+ }
+ }
+ }
+
+ override fun render(): VNode {
+ val text = createLabelWithIcon(text, icon, image)
+ return if (forNavbar || forDropDown) {
+ render("a", text)
+ } else {
+ render("button", text)
+ }
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ return if (forNavbar) {
+ listOf("nav-link" to true, "dropdown-toggle" to true)
+ } else if (forDropDown) {
+ super.getSnClass() + listOf("dropdown-item" to true, "dropdown-toggle" to true)
+ } else {
+ super.getSnClass() + ("dropdown-toggle" to true)
+ }
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ val inherited = super.getSnAttrs()
+ return if (forDropDown || forNavbar) {
+ inherited.filter { it.first != "type" }
+ } else {
+ inherited
+ } + listOf(
+ "data-toggle" to "dropdown", "aria-haspopup" to "true",
+ "aria-expanded" to "false", "href" to "#"
+ )
+ }
+}
+
+internal class DropDownDiv(private val ariaId: String) : Div(
+ null, false, null, setOf("dropdown-menu")
+) {
+ override fun getSnAttrs(): List<StringPair> {
+ return super.getSnAttrs() + listOf("aria-labelledby" to ariaId)
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt
new file mode 100644
index 00000000..13e0b2e4
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.dropdown
+
+import pl.treksoft.kvision.html.TAG
+import pl.treksoft.kvision.html.Tag
+
+/**
+ * Menu header component.
+ *
+ * @constructor
+ * @param content header content text
+ * @param classes a set of CSS class names
+ */
+open class Header(content: String? = null, classes: Set<String> = setOf()) :
+ Tag(TAG.H6, content, classes = classes + "dropdown-header") {
+
+
+ companion object {
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun ContextMenu.header(content: String? = null, classes: Set<String> = setOf()): Header {
+ val header = Header(content, classes)
+ this.add(header)
+ return header
+ }
+
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun DropDown.header(content: String? = null, classes: Set<String> = setOf()): Header {
+ val header = Header(content, classes)
+ this.add(header)
+ return header
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt
new file mode 100644
index 00000000..dd2344bd
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.dropdown
+
+import pl.treksoft.kvision.html.Div
+
+/**
+ * Menu separator component.
+ *
+ * @constructor
+ * @param classes a set of CSS class names
+ */
+open class Separator(classes: Set<String> = setOf()) : Div(classes = classes + "dropdown-divider") {
+
+ companion object {
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun ContextMenu.separator(classes: Set<String> = setOf()): Separator {
+ val separator = Separator(classes)
+ this.add(separator)
+ return separator
+ }
+
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun DropDown.separator(classes: Set<String> = setOf()): Separator {
+ val separator = Separator(classes)
+ this.add(separator)
+ return separator
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Alert.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Alert.kt
new file mode 100644
index 00000000..5f5a1a80
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Alert.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.modal
+
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.html.Align
+import pl.treksoft.kvision.html.ButtonStyle
+import pl.treksoft.kvision.html.Button
+import pl.treksoft.kvision.html.TAG
+import pl.treksoft.kvision.html.Tag
+import pl.treksoft.kvision.utils.ENTER_KEY
+
+/**
+ * Alert window based on Bootstrap modal.
+ *
+ * @constructor
+ * @param caption window title
+ * @param text window content text.
+ * @param rich determines if [text] can contain HTML code
+ * @param align text align
+ * @param size modal window size
+ * @param animation determines if animations are used
+ * @param callback a function called after closing window with OK button
+ */
+open class Alert(
+ caption: String? = null, text: String? = null, rich: Boolean = false,
+ align: Align? = null, size: ModalSize? = null, animation: Boolean = true,
+ private val callback: (() -> Unit)? = null
+) : Modal(caption, true, size, animation) {
+
+ /**
+ * Window content text.
+ */
+ var text
+ get() = contentTag.content
+ set(value) {
+ contentTag.content = value
+ }
+ /**
+ * Determines if [text] can contain HTML code.
+ */
+ var rich
+ get() = contentTag.rich
+ set(value) {
+ contentTag.rich = value
+ }
+ /**
+ * Text align.
+ */
+ var align
+ get() = contentTag.align
+ set(value) {
+ contentTag.align = value
+ }
+
+ private val contentTag = Tag(TAG.DIV, text, rich, align)
+
+ init {
+ body.add(contentTag)
+ val okButton = Button("OK", "ok", ButtonStyle.PRIMARY)
+ okButton.setEventListener {
+ click = {
+ hide()
+ }
+ }
+ this.addButton(okButton)
+ this.setEventListener {
+ keydown = { e ->
+ if (e.keyCode == ENTER_KEY) {
+ hide()
+ }
+ }
+ }
+ }
+
+ override fun hide(): Widget {
+ super.hide()
+ this.callback?.invoke()
+ return this
+ }
+
+ companion object {
+ /**
+ * Helper function for opening Alert window.
+ * @param caption window title
+ * @param text window content text.
+ * @param rich determines if [text] can contain HTML code
+ * @param align text align
+ * @param size modal window size
+ * @param animation determines if animations are used
+ * @param callback a function called after closing window with OK button
+ */
+ @Suppress("LongParameterList")
+ fun show(
+ caption: String? = null, text: String? = null, rich: Boolean = false,
+ align: Align? = null, size: ModalSize? = null, animation: Boolean = true,
+ callback: (() -> Unit)? = null
+ ) {
+ Alert(caption, text, rich, align, size, animation, callback).show()
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/CloseIcon.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/CloseIcon.kt
new file mode 100644
index 00000000..5f0440a6
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/CloseIcon.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.modal
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.KVManager
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.StringPair
+import pl.treksoft.kvision.core.Widget
+
+/**
+ * Helper class for close icon component.
+ */
+open class CloseIcon : Widget(setOf()) {
+
+ override fun render(): VNode {
+ return render("button", arrayOf(KVManager.virtualize("<span aria-hidden='true'>&times;</span>")))
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("close" to true)
+ return cl
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ return super.getSnAttrs() + listOf("type" to "button", "aria-label" to "Close")
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Confirm.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Confirm.kt
new file mode 100644
index 00000000..e16ca87e
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Confirm.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.modal
+
+import pl.treksoft.kvision.html.Align
+import pl.treksoft.kvision.html.Button
+import pl.treksoft.kvision.html.ButtonStyle
+import pl.treksoft.kvision.html.TAG
+import pl.treksoft.kvision.html.Tag
+
+/**
+ * Confirm window based on Bootstrap modal.
+ *
+ * @constructor
+ * @param caption window title
+ * @param text window content text.
+ * @param rich determines if [text] can contain HTML code
+ * @param align text align
+ * @param size modal window size
+ * @param animation determines if animations are used
+ * @param cancelVisible determines if Cancel button is visible
+ * @param yesTitle yes button text
+ * @param noTitle no button text
+ * @param cancelTitle cancel button text
+ * @param noCallback a function called after closing window with No button
+ * @param yesCallback a function called after closing window with Yes button
+ */
+open class Confirm(
+ caption: String? = null, text: String? = null, rich: Boolean = false,
+ align: Align? = null, size: ModalSize? = null, animation: Boolean = true,
+ cancelVisible: Boolean = false, yesTitle: String = "Yes", noTitle: String = "No", cancelTitle: String = "Cancel",
+ private val noCallback: (() -> Unit)? = null,
+ private val yesCallback: (() -> Unit)? = null
+) : Modal(caption, false, size, animation, false) {
+ /**
+ * Window content text.
+ */
+ var text
+ get() = contentTag.content
+ set(value) {
+ contentTag.content = value
+ }
+ /**
+ * Determines if [text] can contain HTML code.
+ */
+ var rich
+ get() = contentTag.rich
+ set(value) {
+ contentTag.rich = value
+ }
+ /**
+ * Text align.
+ */
+ var align
+ get() = contentTag.align
+ set(value) {
+ contentTag.align = value
+ }
+ /**
+ * Determines if Cancel button is visible.
+ */
+ var cancelVisible by refreshOnUpdate(cancelVisible) { refreshCancelButton() }
+
+ /**
+ * Yes button text.
+ */
+ var yesTitle
+ get() = yesButton.text
+ set(value) {
+ yesButton.text = value
+ }
+
+ /**
+ * No button text.
+ */
+ var noTitle
+ get() = noButton.text
+ set(value) {
+ noButton.text = value
+ }
+
+ /**
+ * Cancel button text.
+ */
+ var cancelTitle
+ get() = cancelButton.text
+ set(value) {
+ cancelButton.text = value
+ }
+
+ private val contentTag = Tag(TAG.DIV, text, rich, align)
+ private val cancelButton = Button(cancelTitle, "remove")
+ private val noButton = Button(noTitle, "ban-circle")
+ private val yesButton = Button(yesTitle, "ok", ButtonStyle.PRIMARY)
+
+ init {
+ body.add(contentTag)
+ cancelButton.setEventListener {
+ click = {
+ hide()
+ }
+ }
+ this.addButton(cancelButton)
+ noButton.setEventListener {
+ click = {
+ hide()
+ noCallback?.invoke()
+ }
+ }
+ this.addButton(noButton)
+ yesButton.setEventListener {
+ click = {
+ hide()
+ yesCallback?.invoke()
+ }
+ }
+ this.addButton(yesButton)
+ refreshCancelButton()
+ }
+
+ private fun refreshCancelButton() {
+ if (cancelVisible) {
+ cancelButton.show()
+ closeIcon.show()
+ } else {
+ cancelButton.hide()
+ closeIcon.hide()
+ }
+ }
+
+ companion object {
+ /**
+ * Helper function for opening Confirm window.
+ * @param caption window title
+ * @param text window content text.
+ * @param rich determines if [text] can contain HTML code
+ * @param align text align
+ * @param size modal window size
+ * @param animation determines if animations are used
+ * @param cancelVisible determines if Cancel button is visible
+ * @param noCallback a function called after closing window with No button
+ * @param yesCallback a function called after closing window with Yes button
+ */
+ @Suppress("LongParameterList")
+ fun show(
+ caption: String? = null, text: String? = null, rich: Boolean = false,
+ align: Align? = null, size: ModalSize? = null, animation: Boolean = true,
+ cancelVisible: Boolean = false, yesTitle: String = "Yes", noTitle: String = "No",
+ cancelTitle: String = "Cancel", noCallback: (() -> Unit)? = null, yesCallback: (() -> Unit)? = null
+ ) {
+ Confirm(
+ caption, text, rich, align, size, animation, cancelVisible, yesTitle, noTitle, cancelTitle,
+ noCallback, yesCallback
+ ).show()
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Modal.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Modal.kt
new file mode 100644
index 00000000..08fc7aa8
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/modal/Modal.kt
@@ -0,0 +1,292 @@
+/*
+ * 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.modal
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.core.Component
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.StringPair
+import pl.treksoft.kvision.core.Widget
+import pl.treksoft.kvision.html.Button
+import pl.treksoft.kvision.html.TAG
+import pl.treksoft.kvision.html.Tag
+import pl.treksoft.kvision.panel.Root
+import pl.treksoft.kvision.panel.Root.Companion.addModal
+import pl.treksoft.kvision.panel.Root.Companion.removeModal
+import pl.treksoft.kvision.panel.SimplePanel
+import pl.treksoft.kvision.utils.obj
+
+/**
+ * Modal window sizes.
+ */
+enum class ModalSize(val className: String) {
+ XLARGE("modal-xl"),
+ LARGE("modal-lg"),
+ SMALL("modal-sm")
+}
+
+/**
+ * Configurable modal window based on Bootstrap modal.
+ *
+ * @constructor
+ * @param caption window title
+ * @param closeButton determines if Close button is visible
+ * @param size modal window size
+ * @param animation determines if animations are used
+ * @param escape determines if dialog can be closed with Esc key
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+@Suppress("TooManyFunctions")
+open class Modal(
+ caption: String? = null, closeButton: Boolean = true,
+ size: ModalSize? = null, animation: Boolean = true, private val escape: Boolean = true,
+ classes: Set<String> = setOf(), init: (Modal.() -> Unit)? = null
+) : SimplePanel(classes) {
+
+ override var parent: Container? = Root.getFirstRoot()
+
+ /**
+ * Window caption text.
+ */
+ var caption
+ get() = captionTag.content
+ set(value) {
+ captionTag.content = value
+ checkHeaderVisibility()
+ }
+ /**
+ * Determines if Close button is visible.
+ */
+ var closeButton
+ get() = closeIcon.visible
+ set(value) {
+ closeIcon.visible = value
+ checkHeaderVisibility()
+ }
+ /**
+ * Window size.
+ */
+ var size
+ get() = dialog.size
+ set(value) {
+ dialog.size = value
+ }
+ /**
+ * Determines if animations are used.
+ */
+ var animation by refreshOnUpdate(animation)
+
+ private val dialog = ModalDialog(size)
+ private val header = SimplePanel(setOf("modal-header"))
+ /**
+ * @suppress
+ * Internal property.
+ */
+ protected val closeIcon = CloseIcon()
+ private val captionTag = Tag(TAG.H5, caption, classes = setOf("modal-title"))
+ /**
+ * @suppress
+ * Internal property.
+ */
+ protected val body = SimplePanel(setOf("modal-body"))
+ private val footer = SimplePanel(setOf("modal-footer"))
+
+ init {
+ this.hide()
+ this.role = "dialog"
+ this.tabindex = -1
+ this.addInternal(dialog)
+ val content = SimplePanel(setOf("modal-content"))
+ dialog.role = "document"
+ dialog.add(content)
+ closeIcon.visible = closeButton
+ closeIcon.setEventListener {
+ click = {
+ hide()
+ }
+ }
+ header.add(captionTag)
+ header.add(closeIcon)
+ checkHeaderVisibility()
+ content.add(header)
+ content.add(body)
+ content.add(footer)
+ @Suppress("LeakingThis")
+ addModal(this)
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ private fun checkHeaderVisibility() {
+ if (!closeButton && caption == null) {
+ header.hide()
+ } else {
+ header.show()
+ }
+ }
+
+ override fun add(child: Component): SimplePanel {
+ body.add(child)
+ return this
+ }
+
+ override fun addAll(children: List<Component>): SimplePanel {
+ body.addAll(children)
+ return this
+ }
+
+ override fun remove(child: Component): SimplePanel {
+ body.remove(child)
+ return this
+ }
+
+ override fun removeAll(): SimplePanel {
+ body.removeAll()
+ return this
+ }
+
+ override fun getChildren(): List<Component> {
+ return body.getChildren()
+ }
+
+ /**
+ * Adds given button to the bottom section of dialog window.
+ * @param button a [Button] component
+ * @return this modal
+ */
+ open fun addButton(button: Button): Modal {
+ footer.add(button)
+ return this
+ }
+
+ /**
+ * Removes given button from the bottom section of dialog window.
+ * @param button a [Button] component
+ * @return this modal
+ */
+ open fun removeButton(button: Button): Modal {
+ footer.remove(button)
+ return this
+ }
+
+ /**
+ * Removes all buttons from the bottom section of dialog window.
+ * @return this modal
+ */
+ open fun removeAllButtons(): Modal {
+ footer.removeAll()
+ return this
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("modal" to true)
+ if (animation) {
+ cl.add("fade" to true)
+ }
+ return cl
+ }
+
+ @Suppress("UnsafeCastFromDynamic")
+ override fun afterInsert(node: VNode) {
+ getElementJQueryD()?.modal(obj {
+ keyboard = escape
+ backdrop = if (escape) "true" else "static"
+ })
+ this.getElementJQuery()?.on("show.bs.modal") { e, _ ->
+ this.dispatchEvent("showBsModal", obj { detail = e })
+ }
+ this.getElementJQuery()?.on("shown.bs.modal") { e, _ ->
+ this.dispatchEvent("shownBsModal", obj { detail = e })
+ }
+ this.getElementJQuery()?.on("hide.bs.modal") { e, _ ->
+ this.dispatchEvent("hideBsModal", obj { detail = e })
+ }
+ this.getElementJQuery()?.on("hidden.bs.modal") { e, _ ->
+ this.visible = false
+ hide()
+ this.dispatchEvent("hiddenBsModal", obj { detail = e })
+ }
+ }
+
+ override fun hide(): Widget {
+ if (visible) hideInternal()
+ return super.hide()
+ }
+
+ /**
+ * Toggle modal window visibility.
+ */
+ open fun toggle() {
+ if (visible)
+ hide()
+ else
+ show()
+ }
+
+ @Suppress("UnsafeCastFromDynamic")
+ private fun showInternal() {
+ getElementJQueryD()?.modal("show")
+ }
+
+ @Suppress("UnsafeCastFromDynamic")
+ private fun hideInternal() {
+ getElementJQueryD()?.modal("hide")
+ }
+
+ override fun clearParent(): Widget {
+ this.parent = null
+ return this
+ }
+
+ override fun getRoot(): Root? {
+ return this.parent?.getRoot()
+ }
+
+ override fun dispose() {
+ removeModal(this)
+ }
+}
+
+/**
+ * Internal helper class for modal content.
+ *
+ * @constructor
+ * @param size modal window size
+ */
+internal class ModalDialog(size: ModalSize?) : SimplePanel(setOf("modal-dialog")) {
+
+ /**
+ * Modal window size.
+ */
+ var size by refreshOnUpdate(size)
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ size?.let {
+ cl.add(it.className to true)
+ }
+ return cl
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/Nav.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/Nav.kt
new file mode 100644
index 00000000..e0da480d
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/Nav.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.navbar
+
+import pl.treksoft.kvision.core.ResString
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.html.Div
+import pl.treksoft.kvision.html.Link
+
+/**
+ * The Bootstrap Nav container.
+ *
+ * @constructor
+ * @param rightAlign determines if the nav is aligned to the right
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+open class Nav(rightAlign: Boolean = false, classes: Set<String> = setOf(), init: (Nav.() -> Unit)? = null) :
+ Div(classes = classes) {
+
+ /**
+ * Determines if the nav is aligned to the right.
+ */
+ var rightAlign by refreshOnUpdate(rightAlign)
+
+ init {
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("navbar-nav" to true)
+ if (rightAlign) {
+ cl.add("ml-auto" to true)
+ }
+ return cl
+ }
+
+ companion object {
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Navbar.nav(
+ rightAlign: Boolean = false, classes: Set<String> = setOf(), init: (Nav.() -> Unit)? = null
+ ): Nav {
+ val nav = Nav(rightAlign, classes).apply { init?.invoke(this) }
+ this.add(nav)
+ return nav
+ }
+
+
+ /**
+ * DSL builder extension function for a link in a nav list.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Nav.navLink(
+ label: String, url: String? = null, icon: String? = null, image: ResString? = null,
+ classes: Set<String> = setOf(), init: (Link.() -> Unit)? = null
+ ): Link {
+ val link = Link(label, url, icon, image, classes + "nav-item" + "nav-link").apply {
+ init?.invoke(this)
+ }
+ this.add(link)
+ return link
+ }
+
+ /**
+ * DSL builder extension function for a disabled link in a nav list.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Nav.navLinkDisabled(
+ label: String, icon: String? = null, image: ResString? = null,
+ classes: Set<String> = setOf(), init: (Link.() -> Unit)? = null
+ ): Link {
+ val link =
+ Link(label, "javascript:void(0)", icon, image, classes + "nav-item" + "nav-link" + "disabled").apply {
+ tabindex = -1
+ setAttribute("aria-disabled", "true")
+ init?.invoke(this)
+ }
+ this.add(link)
+ return link
+ }
+
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/NavForm.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/NavForm.kt
new file mode 100644
index 00000000..45454cc8
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/NavForm.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.navbar
+
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.html.TAG
+import pl.treksoft.kvision.html.Tag
+
+/**
+ * The Bootstrap Nav form container.
+ *
+ * @constructor
+ * @param rightAlign determines if the nav form is aligned to the right
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+open class NavForm(rightAlign: Boolean = false, classes: Set<String> = setOf(), init: (NavForm.() -> Unit)? = null) :
+ Tag(TAG.FORM, classes = classes) {
+
+ /**
+ * Determines if the nav form is aligned to the right.
+ */
+ var rightAlign by refreshOnUpdate(rightAlign)
+
+ init {
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("form-inline" to true)
+ if (rightAlign) {
+ cl.add("ml-auto" to true)
+ }
+ return cl
+ }
+
+ companion object {
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Navbar.navForm(
+ rightAlign: Boolean = false, classes: Set<String> = setOf(), init: (NavForm.() -> Unit)? = null
+ ): NavForm {
+ val navForm = NavForm(rightAlign, classes).apply { init?.invoke(this) }
+ this.add(navForm)
+ return navForm
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/Navbar.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/Navbar.kt
new file mode 100644
index 00000000..34a9dbe2
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/navbar/Navbar.kt
@@ -0,0 +1,229 @@
+/*
+ * 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.navbar
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.core.BsBgColor
+import pl.treksoft.kvision.core.Component
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.StringPair
+import pl.treksoft.kvision.html.Link
+import pl.treksoft.kvision.html.Span
+import pl.treksoft.kvision.html.Span.Companion.span
+import pl.treksoft.kvision.panel.SimplePanel
+
+/**
+ * Navbar types.
+ */
+enum class NavbarType(internal val navbarType: String) {
+ FIXEDTOP("fixed-top"),
+ FIXEDBOTTOM("fixed-bottom"),
+ STICKYTOP("sticky-top")
+}
+
+/**
+ * Navbar colors.
+ */
+enum class NavbarColor(internal val navbarColor: String) {
+ LIGHT("navbar-light"),
+ DARK("navbar-dark")
+}
+
+/**
+ * Navbar responsive behavior.
+ */
+enum class NavbarExpand(internal val navbarExpand: String) {
+ ALWAYS("navbar-expand"),
+ XL("navbar-expand-xl"),
+ LG("navbar-expand-lg"),
+ MD("navbar-expand-md"),
+ SM("navbar-expand-sm"),
+}
+
+/**
+ * The Bootstrap Navbar container.
+ *
+ * @constructor
+ * @param label the navbar label
+ * @param type the navbar type
+ * @param expand the navbar responsive behavior
+ * @param nColor the navbar color
+ * @param bgColor the navbar background color
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+open class Navbar(
+ label: String? = null,
+ type: NavbarType? = null,
+ expand: NavbarExpand? = NavbarExpand.LG,
+ nColor: NavbarColor = NavbarColor.LIGHT,
+ bgColor: BsBgColor = BsBgColor.LIGHT,
+ classes: Set<String> = setOf(), init: (Navbar.() -> Unit)? = null
+) : SimplePanel(classes) {
+
+ /**
+ * The navbar header label.
+ */
+ var label
+ get() = if (brandLink.visible) brandLink.label else null
+ set(value) {
+ if (value != null) {
+ brandLink.label = value
+ brandLink.show()
+ } else {
+ brandLink.hide()
+ }
+ }
+
+ /**
+ * The navbar type.
+ */
+ var type by refreshOnUpdate(type)
+ /**
+ * The navbar responsive behavior.
+ */
+ var expand by refreshOnUpdate(expand)
+ /**
+ * The navbar color.
+ */
+ var nColor by refreshOnUpdate(nColor)
+ /**
+ * The navbar background color.
+ */
+ var bgColor by refreshOnUpdate(bgColor)
+
+ private val idc = "kv_navbar_$counter"
+
+ private val brandLink = Link(label ?: "", "#", classes = setOf("navbar-brand"))
+ internal val container = SimplePanel(setOf("collapse", "navbar-collapse")) {
+ id = idc
+ }
+
+ init {
+ addInternal(brandLink)
+ addInternal(NavbarButton(idc))
+ addInternal(container)
+ if (label == null) brandLink.hide()
+ counter++
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ override fun render(): VNode {
+ return render("nav", childrenVNodes())
+ }
+
+ override fun add(child: Component): Navbar {
+ container.add(child)
+ return this
+ }
+
+ override fun addAll(children: List<Component>): Navbar {
+ container.addAll(children)
+ return this
+ }
+
+ override fun remove(child: Component): Navbar {
+ container.remove(child)
+ return this
+ }
+
+ override fun removeAll(): Navbar {
+ container.removeAll()
+ return this
+ }
+
+ override fun getChildren(): List<Component> {
+ return container.getChildren()
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("navbar" to true)
+ type?.let {
+ cl.add(it.navbarType to true)
+ }
+ expand?.let {
+ cl.add(it.navbarExpand to true)
+ }
+ cl.add(nColor.navbarColor to true)
+ cl.add(bgColor.className to true)
+ return cl
+ }
+
+ companion object {
+ internal var counter = 0
+
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.navbar(
+ label: String? = null,
+ type: NavbarType? = null,
+ expand: NavbarExpand? = NavbarExpand.LG,
+ nColor: NavbarColor = NavbarColor.LIGHT,
+ bgColor: BsBgColor = BsBgColor.LIGHT,
+ classes: Set<String> = setOf(), init: (Navbar.() -> Unit)? = null
+ ): Navbar {
+ val navbar = Navbar(label, type, expand, nColor, bgColor, classes, init)
+ this.add(navbar)
+ return navbar
+ }
+
+ fun Navbar.navText(label: String, classes: Set<String> = setOf()): Span {
+ val text = Span(label, classes = classes + "navbar-text")
+ this.add(text)
+ return text
+ }
+ }
+}
+
+/**
+ * @suppress
+ * Internal component.
+ * The Bootstrap Navbar header button.
+ */
+internal class NavbarButton(private val idc: String, private val toggle: String = "Toggle navigation") :
+ SimplePanel(setOf("navbar-toggler")) {
+
+ init {
+ span(classes = setOf("navbar-toggler-icon"))
+ }
+
+ override fun render(): VNode {
+ return render("button", childrenVNodes())
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ return super.getSnAttrs() + listOf(
+ "type" to "button",
+ "data-toggle" to "collapse",
+ "data-target" to "#$idc",
+ "aria-controls" to idc,
+ "aria-expanded" to "false",
+ "aria-label" to toggle
+ )
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel/ResponsiveGridPanel.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel/ResponsiveGridPanel.kt
new file mode 100644
index 00000000..2ff6fa19
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel/ResponsiveGridPanel.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.panel
+
+import pl.treksoft.kvision.core.Component
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.WidgetWrapper
+import pl.treksoft.kvision.html.Align
+import pl.treksoft.kvision.html.TAG
+import pl.treksoft.kvision.html.Tag
+
+/**
+ * Bootstrap grid sizes.
+ */
+enum class GridSize(internal val size: String) {
+ SM("sm"),
+ MD("md"),
+ LG("lg"),
+ XL("xs")
+}
+
+internal const val MAX_COLUMNS = 12
+
+internal data class WidgetParam(val widget: Component, val size: Int, val offset: Int)
+
+/**
+ * The container with support for Bootstrap responsive grid layout.
+ *
+ * @constructor
+ * @param gridSize grid size
+ * @param rows number of rows
+ * @param cols number of columns
+ * @param align text align of grid cells
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+open class ResponsiveGridPanel(
+ private val gridSize: GridSize = GridSize.MD,
+ private var rows: Int = 0, private var cols: Int = 0, align: Align? = null,
+ classes: Set<String> = setOf(), init: (ResponsiveGridPanel.() -> Unit)? = null
+) : SimplePanel(classes + "container-fluid") {
+
+ /**
+ * Text align of grid cells.
+ */
+ var align by refreshOnUpdate(align) { refreshRowContainers() }
+
+ internal val map = mutableMapOf<Int, MutableMap<Int, WidgetParam>>()
+ private var auto: Boolean = true
+
+ init {
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ /**
+ * Adds child component to the grid.
+ * @param child child component
+ * @param col column number
+ * @param row row number
+ * @param size cell size (colspan)
+ * @param offset cell offset
+ * @return this container
+ */
+ open fun add(child: Component, col: Int, row: Int, size: Int = 0, offset: Int = 0): ResponsiveGridPanel {
+ val cRow = maxOf(row, 1)
+ val cCol = maxOf(col, 1)
+ if (cRow > rows) rows = cRow
+ if (cCol > cols) cols = cCol
+ map.getOrPut(cRow) { mutableMapOf() }[cCol] = WidgetParam(child, size, offset)
+ if (size > 0 || offset > 0) auto = false
+ refreshRowContainers()
+ return this
+ }
+
+ override fun add(child: Component): ResponsiveGridPanel {
+ return this.add(child, this.cols, 0)
+ }
+
+ override fun addAll(children: List<Component>): ResponsiveGridPanel {
+ children.forEach { this.add(it) }
+ return this
+ }
+
+ @Suppress("NestedBlockDepth")
+ override fun remove(child: Component): ResponsiveGridPanel {
+ map.values.forEach { row ->
+ row.filterValues { it.widget == child }
+ .forEach { (i, _) -> row.remove(i) }
+ }
+ refreshRowContainers()
+ return this
+ }
+
+ /**
+ * Removes child component at given location (column, row).
+ * @param col column number
+ * @param row row number
+ * @return this container
+ */
+ open fun removeAt(col: Int, row: Int): ResponsiveGridPanel {
+ map[row]?.remove(col)
+ refreshRowContainers()
+ return this
+ }
+
+ @Suppress("ComplexMethod", "NestedBlockDepth")
+ private fun refreshRowContainers() {
+ singleRender {
+ clearRowContainers()
+ val num = MAX_COLUMNS / cols
+ for (i in 1..rows) {
+ val rowContainer = SimplePanel(setOf("row"))
+ val row = map[i]
+ if (row != null) {
+ (1..cols).map { row[it] }.forEach { wp ->
+ if (auto) {
+ val widget = wp?.widget?.let {
+ WidgetWrapper(it, setOf("col-" + gridSize.size + "-" + num))
+ } ?: Tag(TAG.DIV, classes = setOf("col-" + gridSize.size + "-" + num))
+ align?.let {
+ widget.addCssClass(it.className)
+ }
+ rowContainer.add(widget)
+ } else {
+ if (wp != null) {
+ val s = if (wp.size > 0) wp.size else num
+ val widget = WidgetWrapper(wp.widget, setOf("col-" + gridSize.size + "-" + s))
+ if (wp.offset > 0) {
+ widget.addCssClass("offset-" + gridSize.size + "-" + wp.offset)
+ }
+ align?.let {
+ widget.addCssClass(it.className)
+ }
+ rowContainer.add(widget)
+ }
+ }
+ }
+ }
+ addInternal(rowContainer)
+ }
+ }
+ }
+
+ private fun clearRowContainers() {
+ children.forEach { it.dispose() }
+ removeAll()
+ }
+
+ companion object {
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.responsiveGridPanel(
+ gridSize: GridSize = GridSize.MD,
+ rows: Int = 0, cols: Int = 0, align: Align? = null,
+ classes: Set<String> = setOf(), init: (ResponsiveGridPanel.() -> Unit)? = null
+ ): ResponsiveGridPanel {
+ val responsiveGridPanel = ResponsiveGridPanel(gridSize, rows, cols, align, classes, init)
+ this.add(responsiveGridPanel)
+ return responsiveGridPanel
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel/TabPanel.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel/TabPanel.kt
new file mode 100644
index 00000000..2009e4fc
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel/TabPanel.kt
@@ -0,0 +1,273 @@
+/*
+ * 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.panel
+
+import pl.treksoft.kvision.core.Component
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.ResString
+import pl.treksoft.kvision.core.WidgetWrapper
+import pl.treksoft.kvision.html.Icon
+import pl.treksoft.kvision.html.Link.Companion.link
+import pl.treksoft.kvision.html.TAG
+import pl.treksoft.kvision.html.Tag
+import pl.treksoft.kvision.routing.routing
+import pl.treksoft.kvision.utils.obj
+import pl.treksoft.kvision.html.Icon.Companion.icon as cicon
+
+/**
+ * Tab position.
+ */
+enum class TabPosition {
+ TOP,
+ LEFT,
+ RIGHT
+}
+
+/**
+ * Left or right tab size.
+ */
+enum class SideTabSize {
+ SIZE_1,
+ SIZE_2,
+ SIZE_3,
+ SIZE_4,
+ SIZE_5,
+ SIZE_6
+}
+
+/**
+ * The container rendering it's children as tabs.
+ *
+ * It supports activating children by a JavaScript route.
+ *
+ * @constructor
+ * @param tabPosition tab position
+ * @param sideTabSize side tab size
+ * @param scrollableTabs determines if tabs are scrollable (default: false)
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+open class TabPanel(
+ private val tabPosition: TabPosition = TabPosition.TOP,
+ private val sideTabSize: SideTabSize = SideTabSize.SIZE_3,
+ scrollableTabs: Boolean = false,
+ classes: Set<String> = setOf(),
+ init: (TabPanel.() -> Unit)? = null
+) : SimplePanel(classes) {
+
+ /**
+ * The index of active (visible) tab.
+ */
+ var activeIndex
+ get() = content.activeIndex
+ set(value) {
+ if (content.activeIndex != value) {
+ content.activeIndex = value
+ }
+ nav.getChildren().forEach {
+ (it as Tag).getChildren().firstOrNull()?.removeCssClass("active")
+ }
+ if (content.activeIndex in nav.getChildren().indices) {
+ (nav.getChildren()[content.activeIndex] as Tag).getChildren().firstOrNull()?.addCssClass("active")
+ }
+ }
+ private val navClasses = when (tabPosition) {
+ TabPosition.TOP -> if (scrollableTabs) setOf("nav", "nav-tabs", "tabs-top") else setOf("nav", "nav-tabs")
+ TabPosition.LEFT -> setOf("nav", "nav-tabs", "tabs-left", "flex-column")
+ TabPosition.RIGHT -> setOf("nav", "nav-tabs", "tabs-right", "flex-column")
+ }
+ private var nav = Tag(TAG.UL, classes = navClasses)
+ private var content = StackPanel(false)
+
+ internal val childrenMap = mutableMapOf<Int, Component>()
+
+ init {
+ when (tabPosition) {
+ TabPosition.TOP -> {
+ this.addInternal(nav)
+ this.addInternal(content)
+ }
+ TabPosition.LEFT -> {
+ this.addSurroundingCssClass("container-fluid")
+ this.addCssClass("row")
+ val sizes = calculateSideClasses()
+ this.addInternal(WidgetWrapper(nav, setOf(sizes.first, "pl-0", "pr-0")))
+ this.addInternal(WidgetWrapper(content, setOf(sizes.second, "pl-0", "pr-0")))
+ }
+ TabPosition.RIGHT -> {
+ this.addSurroundingCssClass("container-fluid")
+ this.addCssClass("row")
+ val sizes = calculateSideClasses()
+ this.addInternal(WidgetWrapper(content, setOf(sizes.second, "pl-0", "pr-0")))
+ this.addInternal(WidgetWrapper(nav, setOf(sizes.first, "pl-0", "pr-0")))
+ }
+ }
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ private fun calculateSideClasses(): Pair<String, String> {
+ return when (sideTabSize) {
+ SideTabSize.SIZE_1 -> Pair("col-sm-1", "col-sm-11")
+ SideTabSize.SIZE_2 -> Pair("col-sm-2", "col-sm-10")
+ SideTabSize.SIZE_3 -> Pair("col-sm-3", "col-sm-9")
+ SideTabSize.SIZE_4 -> Pair("col-sm-4", "col-sm-8")
+ SideTabSize.SIZE_5 -> Pair("col-sm-5", "col-sm-7")
+ SideTabSize.SIZE_6 -> Pair("col-sm-6", "col-sm-6")
+ }
+ }
+
+ /**
+ * Adds new tab and optionally bounds it's activation to a given route.
+ * @param title title of the tab
+ * @param panel child component
+ * @param icon icon of the tab
+ * @param image image of the tab
+ * @param closable determines if this tab is closable
+ * @param route JavaScript route to activate given child
+ * @return current container
+ */
+ open fun addTab(
+ title: String, panel: Component, icon: String? = null,
+ image: ResString? = null, closable: Boolean = false, route: String? = null
+ ): TabPanel {
+ val currentIndex = counter++
+ childrenMap[currentIndex] = panel
+ val tag = Tag(TAG.LI, classes = setOf("nav-item")) {
+ link(title, "#", icon, image, classes = setOf("nav-link")) {
+ if (closable) {
+ cicon("fas fa-times") {
+ addCssClass("kv-tab-close")
+ setEventListener<Icon> {
+ click = { e ->
+ val actIndex = this@TabPanel.content.getChildren().indexOf(childrenMap[currentIndex])
+ e.asDynamic().data = actIndex
+ @Suppress("UnsafeCastFromDynamic")
+ if (this@TabPanel.dispatchEvent(
+ "tabClosing",
+ obj { detail = e; cancelable = true }) != false
+ ) {
+ this@TabPanel.removeTab(actIndex)
+ this@TabPanel.dispatchEvent("tabClosed", obj { detail = e })
+ }
+ e.stopPropagation()
+ }
+ }
+ }
+ }
+ }
+ setEventListener {
+ click = { e ->
+ activeIndex = this@TabPanel.content.getChildren().indexOf(childrenMap[currentIndex])
+ e.preventDefault()
+ if (route != null) {
+ routing.navigate(route)
+ }
+ }
+ }
+ }
+ nav.add(tag)
+ if (nav.getChildren().size == 1) {
+ tag.getChildren().firstOrNull()?.addCssClass("active")
+ activeIndex = 0
+ }
+ content.add(panel)
+ if (route != null) {
+ routing.on(
+ route,
+ { _ -> activeIndex = this@TabPanel.content.getChildren().indexOf(childrenMap[currentIndex]) })
+ .resolve()
+ }
+ return this
+ }
+
+ /**
+ * Removes tab at given index.
+ */
+ open fun removeTab(index: Int): TabPanel {
+ nav.remove(nav.getChildren()[index])
+ childrenMap.filter { it.value == content.getChildren()[index] }.keys.firstOrNull()?.let {
+ childrenMap.remove(it)
+ }
+ content.remove(content.getChildren()[index])
+ activeIndex = content.activeIndex
+ return this
+ }
+
+ override fun add(child: Component): TabPanel {
+ return addTab("", child)
+ }
+
+ override fun addAll(children: List<Component>): TabPanel {
+ children.forEach { add(it) }
+ return this
+ }
+
+ override fun remove(child: Component): TabPanel {
+ val index = content.getChildren().indexOf(child)
+ return removeTab(index)
+ }
+
+ /**
+ * Returns child component by tab index.
+ * @param index tab index
+ */
+ open fun getChildComponent(index: Int): Component? {
+ return content.getChildren()[index]
+ }
+
+ /**
+ * Returns tab header component by tab index.
+ * @param index tab index
+ */
+ open fun getNavComponent(index: Int): Tag? {
+ return nav.getChildren()[index] as? Tag
+ }
+
+ override fun removeAll(): TabPanel {
+ content.removeAll()
+ nav.removeAll()
+ childrenMap.clear()
+ refresh()
+ return this
+ }
+
+ companion object {
+ internal var counter = 0
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.tabPanel(
+ tabPosition: TabPosition = TabPosition.TOP,
+ sideTabSize: SideTabSize = SideTabSize.SIZE_3,
+ scrollableTabs: Boolean = false,
+ classes: Set<String> = setOf(),
+ init: (TabPanel.() -> Unit)? = null
+ ): TabPanel {
+ val tabPanel = TabPanel(tabPosition, sideTabSize, scrollableTabs, classes, init)
+ this.add(tabPanel)
+ return tabPanel
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/progress/ProgressBar.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/progress/ProgressBar.kt
new file mode 100644
index 00000000..4d0f4b93
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/progress/ProgressBar.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.progress
+
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.html.Align
+import pl.treksoft.kvision.panel.SimplePanel
+
+/**
+ * The Bootstrap progress bar.
+ *
+ * @constructor
+ * @param progress the current progress
+ * @param min the minimal progress
+ * @param max the maximal progress
+ * @param style the style of the progress bar
+ * @param striped determines if the progress bar is striped
+ * @param animated determines if the progress bar is animated
+ * @param content element text
+ * @param rich determines if content can contain HTML code
+ * @param align content align
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+open class ProgressBar(
+ progress: Int, min: Int = DEFAULT_MIN, max: Int = DEFAULT_MAX, style: ProgressBarStyle? = null,
+ striped: Boolean = false, animated: Boolean = false, content: String? = null,
+ rich: Boolean = false, align: Align? = null,
+ classes: Set<String> = setOf(), init: (ProgressBar.() -> Unit)? = null
+) :
+ SimplePanel(classes + "progress") {
+
+ /**
+ * The current progress.
+ */
+ var progress
+ get() = indicator.progress
+ set(value) {
+ indicator.progress = value
+ }
+ /**
+ * The minimal progress.
+ */
+ var min
+ get() = indicator.min
+ set(value) {
+ indicator.min = value
+ }
+ /**
+ * The maximal progress.
+ */
+ var max
+ get() = indicator.max
+ set(value) {
+ indicator.max = value
+ }
+ /**
+ * The style of the progress bar.
+ */
+ var style
+ get() = indicator.style
+ set(value) {
+ indicator.style = value
+ }
+ /**
+ * Determines if the progress bar is striped.
+ */
+ var striped
+ get() = indicator.striped
+ set(value) {
+ indicator.striped = value
+ }
+ /**
+ * Determines if the progress bar is animated.
+ */
+ var animated
+ get() = indicator.animated
+ set(value) {
+ indicator.animated = value
+ }
+ /**
+ * Text content of the progress bar.
+ */
+ var content
+ get() = indicator.content
+ set(value) {
+ indicator.content = value
+ }
+ /**
+ * Determines if [content] can contain HTML code.
+ */
+ var rich
+ get() = indicator.rich
+ set(value) {
+ indicator.rich = value
+ }
+ /**
+ * Text align of the progress bar.
+ */
+ var align
+ get() = indicator.align
+ set(value) {
+ indicator.align = value
+ }
+
+ internal val indicator = ProgressIndicator(progress, min, max, style, striped, animated, content, rich, align)
+
+ init {
+ addInternal(indicator)
+
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ companion object {
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.progressBar(
+ progress: Int, min: Int = DEFAULT_MIN, max: Int = DEFAULT_MAX, style: ProgressBarStyle? = null,
+ striped: Boolean = false, animated: Boolean = false,
+ content: String? = null, rich: Boolean = false, align: Align? = null,
+ classes: Set<String> = setOf(), init: (ProgressBar.() -> Unit)? = null
+ ): ProgressBar {
+ val progressBar = ProgressBar(
+ progress,
+ min,
+ max,
+ style,
+ striped,
+ animated,
+ content,
+ rich,
+ align,
+ classes
+ ).apply { init?.invoke(this) }
+ this.add(progressBar)
+ return progressBar
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/progress/ProgressIndicator.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/progress/ProgressIndicator.kt
new file mode 100644
index 00000000..256d15d7
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/progress/ProgressIndicator.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.progress
+
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.StringPair
+import pl.treksoft.kvision.html.Align
+import pl.treksoft.kvision.html.Div
+import pl.treksoft.kvision.utils.perc
+
+/**
+ * Progress bar styles.
+ */
+enum class ProgressBarStyle(internal val className: String) {
+ SUCCESS("progress-bar-success"),
+ INFO("progress-bar-info"),
+ WARNING("progress-bar-warning"),
+ DANGER("progress-bar-danger")
+}
+
+internal const val DEFAULT_MIN = 0
+internal const val DEFAULT_MAX = 100
+
+
+/**
+ * The Bootstrap progress bar indicator.
+ *
+ * @constructor
+ * @param progress the current progress
+ * @param min the minimal progress
+ * @param max the maximal progress
+ * @param style the style of the progress bar indicator
+ * @param striped determines if the progress bar indicator is striped
+ * @param animated determines if the progress bar indicator is animated
+ * @param content element text
+ * @param rich determines if [content] can contain HTML code
+ * @param align content align
+ * @param classes a set of CSS class names
+ */
+internal class ProgressIndicator(
+ progress: Int, min: Int = DEFAULT_MIN, max: Int = DEFAULT_MAX, style: ProgressBarStyle? = null,
+ striped: Boolean = false, animated: Boolean = false,
+ content: String? = null, rich: Boolean = false, align: Align? = null,
+ classes: Set<String> = setOf()
+) :
+ Div(content, rich, align, classes) {
+
+ /**
+ * The current progress.
+ */
+ var progress by refreshOnUpdate(progress) { refreshWidth() }
+ /**
+ * The minimal progress.
+ */
+ var min by refreshOnUpdate(min) { refreshWidth() }
+ /**
+ * The maximal progress.
+ */
+ var max by refreshOnUpdate(max) { refreshWidth() }
+ /**
+ * The style of the progress indicator.
+ */
+ var style by refreshOnUpdate(style)
+ /**
+ * Determines if the progress indicator is striped.
+ */
+ var striped by refreshOnUpdate(striped)
+ /**
+ * Determines if the progress indicator is animated.
+ */
+ var animated by refreshOnUpdate(animated)
+
+ init {
+ role = "progressbar"
+ refreshWidth()
+ }
+
+ private fun refreshWidth() {
+ val value = (if (max - min > 0) (progress - min) * DEFAULT_MAX.toFloat() / (max - min) else 0f).toInt()
+ val percent = if (value < 0) 0 else if (value > DEFAULT_MAX) DEFAULT_MAX else value
+ width = percent.perc
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("progress-bar" to true)
+ style?.let {
+ cl.add(it.className to true)
+ }
+ if (striped || animated) {
+ cl.add("progress-bar-striped" to true)
+ }
+ if (animated) {
+ cl.add("active" to true)
+ }
+ return cl
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ val sn = super.getSnAttrs().toMutableList()
+ sn.add("aria-valuenow" to "$progress")
+ sn.add("aria-valuemin" to "$min")
+ sn.add("aria-valuemax" to "$max")
+ return sn
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/toolbar/ButtonGroup.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/toolbar/ButtonGroup.kt
new file mode 100644
index 00000000..2aef9e63
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/toolbar/ButtonGroup.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 pl.treksoft.kvision.toolbar
+
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.panel.SimplePanel
+import pl.treksoft.kvision.utils.px
+
+/**
+ * Button group sizes.
+ */
+enum class ButtonGroupSize(internal val className: String) {
+ LARGE("btn-group-lg"),
+ SMALL("btn-group-sm")
+}
+
+/**
+ * The Bootstrap button group.
+ *
+ * @constructor
+ * @param size button group size
+ * @param vertical determines if button group is aligned vertically
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+open class ButtonGroup(
+ size: ButtonGroupSize? = null, vertical: Boolean = false,
+ classes: Set<String> = setOf(), init: (ButtonGroup.() -> Unit)? = null
+) : SimplePanel(classes) {
+
+ /**
+ * Button group size.
+ */
+ var size by refreshOnUpdate(size)
+ /**
+ * Vertical alignment.
+ */
+ var vertical by refreshOnUpdate(vertical)
+
+ init {
+ role = "group"
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ if (vertical) {
+ cl.add("btn-group-vertical" to true)
+ } else {
+ cl.add("btn-group" to true)
+ }
+ size?.let {
+ cl.add(it.className to true)
+ }
+ return cl
+ }
+
+ companion object {
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.buttonGroup(
+ size: ButtonGroupSize? = null, vertical: Boolean = false,
+ classes: Set<String> = setOf(), init: (ButtonGroup.() -> Unit)? = null
+ ): ButtonGroup {
+ val group = ButtonGroup(size, vertical, classes).apply { init?.invoke(this) }
+ this.add(group)
+ return group
+ }
+ /**
+ * DSL builder extension function for toolbar.
+ *
+ * It creates button groups with size and vertical parameters of the toolbar.
+ */
+ fun Toolbar.buttonGroup(
+ classes: Set<String> = setOf(), init: (ButtonGroup.() -> Unit)? = null
+ ): ButtonGroup {
+ val group = ButtonGroup(this.size, this.vertical, classes).apply {
+ marginRight = this@buttonGroup.spacing.px
+ init?.invoke(this)
+ }
+ this.add(group)
+ return group
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/toolbar/Toolbar.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/toolbar/Toolbar.kt
new file mode 100644
index 00000000..13ed8972
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/toolbar/Toolbar.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.toolbar
+
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.panel.SimplePanel
+
+/**
+ * The Bootstrap toolbar.
+ *
+ * @constructor
+ * @param size button groups size
+ * @param spacing the spacing between button groups
+ * @param vertical determines if button groups are aligned vertically
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+open class Toolbar(
+ val size: ButtonGroupSize? = null, val spacing: Int = 5, val vertical: Boolean = false,
+ classes: Set<String> = setOf(), init: (Toolbar.() -> Unit)? = null
+) : SimplePanel(classes + "btn-toolbar") {
+
+ init {
+ role = "toolbar"
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ }
+
+ companion object {
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.toolbar(
+ size: ButtonGroupSize? = null, spacing: Int = 2, vertical: Boolean = false,
+ classes: Set<String> = setOf(), init: (Toolbar.() -> Unit)? = null
+ ): Toolbar {
+ val toolbar = Toolbar(size, spacing, vertical, classes).apply { init?.invoke(this) }
+ this.add(toolbar)
+ return toolbar
+ }
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/MaximizeIcon.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/MaximizeIcon.kt
new file mode 100644
index 00000000..a3ceaf61
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/MaximizeIcon.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.window
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.KVManager
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.StringPair
+import pl.treksoft.kvision.core.Widget
+
+/**
+ * Helper class for maximize icon component.
+ */
+open class MaximizeIcon : Widget(setOf()) {
+
+ override fun render(): VNode {
+ return render("button", arrayOf(KVManager.virtualize("<span aria-hidden='true'>&#x1f5d6;</span>")))
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("close" to true)
+ return cl
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ return super.getSnAttrs() + listOf("type" to "button", "aria-label" to "Maximize")
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/MinimizeIcon.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/MinimizeIcon.kt
new file mode 100644
index 00000000..c8034d09
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/MinimizeIcon.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.window
+
+import com.github.snabbdom.VNode
+import pl.treksoft.kvision.KVManager
+import pl.treksoft.kvision.core.StringBoolPair
+import pl.treksoft.kvision.core.StringPair
+import pl.treksoft.kvision.core.Widget
+
+/**
+ * Helper class for minimize icon component.
+ */
+open class MinimizeIcon : Widget(setOf()) {
+
+ override fun render(): VNode {
+ return render("button", arrayOf(KVManager.virtualize("<span aria-hidden='true'>&#x1f5d5;</span>")))
+ }
+
+ override fun getSnClass(): List<StringBoolPair> {
+ val cl = super.getSnClass().toMutableList()
+ cl.add("close" to true)
+ return cl
+ }
+
+ override fun getSnAttrs(): List<StringPair> {
+ return super.getSnAttrs() + listOf("type" to "button", "aria-label" to "Minimize")
+ }
+}
diff --git a/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/Window.kt b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/Window.kt
new file mode 100644
index 00000000..83473858
--- /dev/null
+++ b/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/window/Window.kt
@@ -0,0 +1,449 @@
+/*
+ * 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.window
+
+import com.github.snabbdom.VNode
+import org.w3c.dom.events.Event
+import org.w3c.dom.events.MouseEvent
+import pl.treksoft.kvision.KVManager
+import pl.treksoft.kvision.KVManagerBootstrap
+import pl.treksoft.kvision.core.Component
+import pl.treksoft.kvision.core.Container
+import pl.treksoft.kvision.core.CssSize
+import pl.treksoft.kvision.core.Overflow
+import pl.treksoft.kvision.core.Position
+import pl.treksoft.kvision.core.Resize
+import pl.treksoft.kvision.core.UNIT
+import pl.treksoft.kvision.html.Icon
+import pl.treksoft.kvision.html.TAG
+import pl.treksoft.kvision.html.Tag
+import pl.treksoft.kvision.modal.CloseIcon
+import pl.treksoft.kvision.panel.SimplePanel
+import pl.treksoft.kvision.utils.obj
+import pl.treksoft.kvision.utils.px
+
+internal const val DEFAULT_Z_INDEX = 900
+internal const val WINDOW_HEADER_HEIGHT = 40
+internal const val WINDOW_CONTENT_MARGIN_BOTTOM = 11
+
+/**
+ * Floating window container.
+ *
+ * @constructor
+ * @param caption window title
+ * @param contentWidth window content width
+ * @param contentHeight window content height
+ * @param isResizable determines if the window is resizable
+ * @param isDraggable determines if the window is draggable
+ * @param closeButton determines if Close button is visible
+ * @param maximizeButton determines if Maximize button is visible
+ * @param minimizeButton determines if Minimize button is visible
+ * @param classes a set of CSS class names
+ * @param init an initializer extension function
+ */
+@Suppress("TooManyFunctions")
+open class Window(
+ caption: String? = null,
+ contentWidth: CssSize? = CssSize(0, UNIT.auto),
+ contentHeight: CssSize? = CssSize(0, UNIT.auto),
+ isResizable: Boolean = true,
+ isDraggable: Boolean = true,
+ closeButton: Boolean = false,
+ maximizeButton: Boolean = false,
+ minimizeButton: Boolean = false,
+ icon: String? = null,
+ classes: Set<String> = setOf(),
+ init: (Window.() -> Unit)? = null
+) :
+ SimplePanel(classes + setOf("modal-content", "kv-window")) {
+
+ /**
+ * Window caption text.
+ */
+ var caption
+ get() = captionTag.content
+ set(value) {
+ captionTag.content = value
+ checkHeaderVisibility()
+ }
+ /**
+ * Window content width.
+ */
+ var contentWidth
+ get() = width
+ set(value) {
+ width = value
+ }
+ /**
+ * Window content height.
+ */
+ var contentHeight
+ get() = content.height
+ set(value) {
+ content.height = value
+ }
+ /**
+ * Window content height.
+ */
+ var contentOverflow
+ get() = content.overflow
+ set(value) {
+ content.overflow = value
+ }
+ /**
+ * Determines if the window is resizable.
+ */
+ var isResizable by refreshOnUpdate(isResizable) { checkIsResizable() }
+ /**
+ * Determines if the window is draggable.
+ */
+ var isDraggable by refreshOnUpdate(isDraggable) { checkIsDraggable(); checkHeaderVisibility() }
+ /**
+ * Determines if Close button is visible.
+ */
+ var closeButton
+ get() = closeIcon.visible
+ set(value) {
+ closeIcon.visible = value
+ checkHeaderVisibility()
+ }
+ /**
+ * Determines if Maximize button is visible.
+ */
+ var maximizeButton
+ get() = maximizeIcon.visible
+ set(value) {
+ maximizeIcon.visible = value
+ checkHeaderVisibility()
+ }
+ /**
+ * Determines if Maximize button is visible.
+ */
+ var minimizeButton
+ get() = minimizeIcon.visible
+ set(value) {
+ minimizeIcon.visible = value
+ checkHeaderVisibility()
+ }
+ /**
+ * Window icon.
+ */
+ var icon
+ get() = if (windowIcon.icon == "") null else windowIcon.icon
+ set(value) {
+ windowIcon.icon = value ?: ""
+ windowIcon.visible = (value != null && value != "")
+ }
+
+ private val header = SimplePanel(setOf("modal-header"))
+
+ /**
+ * @suppress
+ * Internal property.
+ */
+ protected val content = SimplePanel().apply {
+ this.height = contentHeight
+ this.overflow = Overflow.AUTO
+ }
+ private val closeIcon = CloseIcon()
+ private val maximizeIcon = MaximizeIcon()
+ private val minimizeIcon = MinimizeIcon()
+ private val captionTag = Tag(TAG.H5, caption, classes = setOf("modal-title"))
+ private val iconsContainer = SimplePanel(setOf("kv-window-icons-container"))
+ private val windowIcon = Icon(icon ?: "").apply {
+ addCssClass("window-icon")
+ visible = (icon != null && icon != "")
+ }
+
+ private var isResizeEvent = false
+
+ init {
+ id = "kv_window_$counter"
+ @Suppress("LeakingThis")
+ position = Position.ABSOLUTE
+ @Suppress("LeakingThis")
+ overflow = Overflow.HIDDEN
+ @Suppress("LeakingThis")
+ width = contentWidth
+ @Suppress("LeakingThis")
+ zIndex = ++zIndexCounter
+ header.add(captionTag)
+ captionTag.add(windowIcon)
+ header.add(iconsContainer)
+ minimizeIcon.visible = minimizeButton
+ minimizeIcon.setEventListener {
+ click = { _ ->
+ @Suppress("UnsafeCastFromDynamic")
+ if (this@Window.dispatchEvent("minimizeWindow", obj {}) != false) {
+ toggleMinimize()
+ }
+ }
+ mousedown = { e ->
+ e.stopPropagation()
+ }
+ }
+ iconsContainer.add(minimizeIcon)
+ maximizeIcon.visible = maximizeButton
+ maximizeIcon.setEventListener {
+ click = { _ ->
+ @Suppress("UnsafeCastFromDynamic")
+ if (this@Window.dispatchEvent("maximizeWindow", obj {}) != false) {
+ toggleMaximize()
+ }
+ }
+ mousedown = { e ->
+ e.stopPropagation()
+ }
+ }
+ iconsContainer.add(maximizeIcon)
+ closeIcon.visible = closeButton
+ closeIcon.setEventListener {
+ click = { _ ->
+ @Suppress("UnsafeCastFromDynamic")
+ if (this@Window.dispatchEvent("closeWindow", obj {}) != false) {
+ close()
+ }
+ }
+ mousedown = { e ->
+ e.stopPropagation()
+ }
+ }
+ iconsContainer.add(closeIcon)
+ checkHeaderVisibility()
+ addInternal(header)
+ addInternal(content)
+ checkIsDraggable()
+ if (isResizable) {
+ @Suppress("LeakingThis")
+ resize = Resize.BOTH
+ content.marginBottom = WINDOW_CONTENT_MARGIN_BOTTOM.px
+ }
+ @Suppress("LeakingThis")
+ setEventListener<Window> {
+ click = {
+ toFront()
+ focus()
+ }
+ }
+ @Suppress("LeakingThis")
+ init?.invoke(this)
+ counter++
+ }
+
+ private fun checkHeaderVisibility() {
+ @Suppress("ComplexCondition")
+ if (!closeButton && !maximizeButton && !minimizeButton && caption == null && !isDraggable) {
+ header.hide()
+ } else {
+ header.show()
+ }
+ }
+
+ private fun checkIsDraggable() {
+ var isDrag: Boolean
+ if (isDraggable) {
+ header.setEventListener<SimplePanel> {
+ mousedown = { e ->
+ if (e.button.toInt() == 0) {
+ isDrag = true
+ val dragStartX = this@Window.getElementJQuery()?.position()?.left?.toInt() ?: 0
+ val dragStartY = this@Window.getElementJQuery()?.position()?.top?.toInt() ?: 0
+ val dragMouseX = e.pageX
+ val dragMouseY = e.pageY
+ val moveCallback = { me: Event ->
+ if (isDrag) {
+ this@Window.left = (dragStartX + (me as MouseEvent).pageX - dragMouseX).toInt().px
+ this@Window.top = (dragStartY + (me).pageY - dragMouseY).toInt().px
+ }
+ }
+ kotlin.browser.window.addEventListener("mousemove", moveCallback)
+ var upCallback: ((Event) -> Unit)? = null
+ upCallback = {
+ isDrag = false
+ kotlin.browser.window.removeEventListener("mousemove", moveCallback)
+ kotlin.browser.window.removeEventListener("mouseup", upCallback)
+ }
+ kotlin.browser.window.addEventListener("mouseup", upCallback)
+ }
+ }
+ }
+ } else {
+ isDrag = false
+ header.removeEventListeners()
+ }
+ }
+
+ private fun checkIsResizable() {
+ checkResizablEventHandler()
+ if (isResizable) {
+ resize = Resize.BOTH
+ val intHeight = (getElementJQuery()?.height()?.toInt() ?: 0)
+ content.height = (intHeight - WINDOW_HEADER_HEIGHT - WINDOW_CONTENT_MARGIN_BOTTOM).px
+ content.marginBottom = WINDOW_CONTENT_MARGIN_BOTTOM.px
+ } else {
+ resize = Resize.NONE
+ val intHeight = (getElementJQuery()?.height()?.toInt() ?: 0)
+ content.height = (intHeight - WINDOW_HEADER_HEIGHT).px
+ content.marginBottom = 0.px
+ }
+ }
+
+ @Suppress("UnsafeCastFromDynamic")
+ private fun checkResizablEventHandler() {
+ if (isResizable) {
+ if (!isResizeEvent) {
+ isResizeEvent = true
+ KVManagerBootstrap.setResizeEvent(this) {
+ val eid = getElementJQuery()?.attr("id")
+ if (isResizable && eid == id) {
+ val outerWidth = (getElementJQuery()?.outerWidth()?.toInt() ?: 0)
+ val outerHeight = (getElementJQuery()?.outerHeight()?.toInt() ?: 0)
+ val intWidth = (getElementJQuery()?.width()?.toInt() ?: 0)
+ val intHeight = (getElementJQuery()?.height()?.toInt() ?: 0)
+ content.width = intWidth.px
+ content.height = (intHeight - WINDOW_HEADER_HEIGHT - WINDOW_CONTENT_MARGIN_BOTTOM).px
+ width = outerWidth.px
+ height = outerHeight.px
+ this.dispatchEvent("resizeWindow", obj {
+ detail = obj {
+ this.width = outerWidth
+ this.height = outerHeight
+ }
+ })
+ }
+ }
+ }
+ } else if (isResizeEvent) {
+ KVManagerBootstrap.clearResizeEvent(this)
+ isResizeEvent = false
+ }
+ }
+
+ override fun add(child: Component): SimplePanel {
+ content.add(child)
+ return this
+ }
+
+ override fun addAll(children: List<Component>): SimplePanel {
+ content.addAll(children)
+ return this
+ }
+
+ override fun remove(child: Component): SimplePanel {
+ content.remove(child)
+ return this
+ }
+
+ override fun removeAll(): SimplePanel {
+ content.removeAll()
+ return this
+ }
+
+ override fun getChildren(): List<Component> {
+ return content.getChildren()
+ }
+
+ override fun afterCreate(node: VNode) {
+ checkResizablEventHandler()
+ }
+
+ override fun afterDestroy() {
+ if (isResizeEvent) {
+ KVManagerBootstrap.clearResizeEvent(this)
+ isResizeEvent = false
+ }
+ }
+
+ /**
+ * Moves the current window to the front.
+ */
+ open fun toFront() {
+ if ((zIndex ?: 0) < zIndexCounter) zIndex = ++zIndexCounter
+ }
+
+ /**
+ * Makes the current window focused.
+ */
+ open fun focus() {
+ getElementJQuery()?.focus()
+ }
+
+ /**
+ * Close the window.
+ */
+ open fun close() {
+ hide()
+ }
+
+ /**
+ * Maximize or restore the window size.
+ */
+ open fun toggleMaximize() {
+ }
+
+ /**
+ * Minimize or restore the window size.
+ */
+ open fun toggleMinimize() {
+ }
+
+ companion object {
+ internal var counter = 0
+ internal var zIndexCounter = DEFAULT_Z_INDEX
+
+ /**
+ * DSL builder extension function.
+ *
+ * It takes the same parameters as the constructor of the built component.
+ */
+ fun Container.window(
+ caption: String? = null,
+ contentWidth: CssSize? = CssSize(0, UNIT.auto),
+ contentHeight: CssSize? = CssSize(0, UNIT.auto),
+ isResizable: Boolean = true,
+ isDraggable: Boolean = true,
+ closeButton: Boolean = false,
+ maximizeButton: Boolean = false,
+ minimizeButton: Boolean = false,
+ icon: String? = null,
+ classes: Set<String> = setOf(),
+ init: (Window.() -> Unit)? = null
+ ): Window {
+ val window =
+ Window(
+ caption,
+ contentWidth,
+ contentHeight,
+ isResizable,
+ isDraggable,
+ closeButton,
+ maximizeButton,
+ minimizeButton,
+ icon,
+ classes,
+ init
+ )
+ this.add(window)
+ return window
+ }
+ }
+}