aboutsummaryrefslogtreecommitdiff
path: root/kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel
diff options
context:
space:
mode:
Diffstat (limited to 'kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel')
-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
2 files changed, 458 insertions, 0 deletions
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
+ }
+ }
+}