diff options
Diffstat (limited to 'kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft/kvision/panel')
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 + } + } +} |