diff options
author | Robert Jaros <rjaros@finn.pl> | 2019-10-07 09:58:14 +0200 |
---|---|---|
committer | Robert Jaros <rjaros@finn.pl> | 2019-10-07 09:58:14 +0200 |
commit | 04ac8542c218b7ce5199350f0880e8f7cb4252b6 (patch) | |
tree | 4f96d1c3bb8281289b96e2b11eecc404a3c98788 /kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft | |
parent | 6678eec9799681b09e5ac85de1a39596d56de22f (diff) | |
parent | 6b14906f0e35dc522bd1c1a44682d728315cf619 (diff) | |
download | kvision-04ac8542c218b7ce5199350f0880e8f7cb4252b6.tar.gz kvision-04ac8542c218b7ce5199350f0880e8f7cb4252b6.tar.bz2 kvision-04ac8542c218b7ce5199350f0880e8f7cb4252b6.zip |
Merge branch 'bs4'
Diffstat (limited to 'kvision-modules/kvision-bootstrap/src/main/kotlin/pl/treksoft')
22 files changed, 3318 insertions, 26 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 01e1f3c3..78365281 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,39 +21,41 @@ */ 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 } - if (bootswatch != null) { - require("bootstrap-webpack!./js/bootstrap.config.js") - if (bootswatch.getAttribute("href")?.contains("/paper/") == true) { - require("./css/paper.css") + init { + require("bootstrap/dist/js/bootstrap.bundle.min.js") + require("awesome-bootstrap-checkbox") + } + + private val elementResizeEvent = require("element-resize-event") + + @Suppress("UnsafeCastFromDynamic") + internal fun setResizeEvent(component: Component, callback: () -> Unit) { + if (!isIE11()) { + component.getElement()?.let { + elementResizeEvent(it, callback) } - require("./css/style.css") - } else { - require("bootstrap-webpack") - 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..4c5b222e --- /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", "fas fa-check", 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'>×</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..058ca7bc --- /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, "fas fa-times") + private val noButton = Button(noTitle, "fas fa-ban") + private val yesButton = Button(yesTitle, "fas fa-check", 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'>🗖</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'>🗕</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 + } + } +} |