From 7081cfaed23fe8b34bfdf15918775a846d7649e0 Mon Sep 17 00:00:00 2001 From: Robert Jaros Date: Thu, 22 Mar 2018 20:23:54 +0100 Subject: Context menu component based on Bootstrap dropdown. --- src/main/kotlin/pl/treksoft/kvision/core/Widget.kt | 19 ++++++- .../pl/treksoft/kvision/dropdown/ContextMenu.kt | 66 ++++++++++++++++++++++ .../pl/treksoft/kvision/dropdown/DropDown.kt | 20 ++++--- .../kotlin/pl/treksoft/kvision/dropdown/Header.kt | 44 +++++++++++++++ .../pl/treksoft/kvision/dropdown/Separator.kt | 48 ++++++++++++++++ src/main/kotlin/pl/treksoft/kvision/html/Link.kt | 33 +++++++++++ src/main/kotlin/pl/treksoft/kvision/html/List.kt | 5 +- src/main/kotlin/pl/treksoft/kvision/panel/Root.kt | 22 +++++++- 8 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt create mode 100644 src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt create mode 100644 src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt (limited to 'src/main') diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt index 03222d6f..61e9ba4e 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt @@ -27,9 +27,11 @@ import com.github.snabbdom.h import org.w3c.dom.CustomEventInit import org.w3c.dom.DragEvent import org.w3c.dom.Node +import org.w3c.dom.events.MouseEvent import pl.treksoft.jquery.JQuery import pl.treksoft.jquery.jQuery import pl.treksoft.kvision.KVManager +import pl.treksoft.kvision.dropdown.ContextMenu import pl.treksoft.kvision.panel.Root import pl.treksoft.kvision.utils.SnOn import pl.treksoft.kvision.utils.hooks @@ -55,7 +57,7 @@ open class Widget(classes: Set = setOf()) : StyledComponent() { internal val internalListeners = mutableListOf.() -> Unit>() internal val listeners = mutableListOf.() -> Unit>() - override var parent: Component? = null + override var parent: Container? = null override var visible: Boolean = true set(value) { @@ -541,6 +543,21 @@ open class Widget(classes: Set = setOf()) : StyledComponent() { } } + /** + * Sets context menu for the current widget. + * @param contextMenu a context menu + * @return current widget + */ + open fun setContextMenu(contextMenu: ContextMenu): Widget { + setEventListener { + contextmenu = { e: MouseEvent -> + e.preventDefault() + contextMenu.positionMenu(e) + } + } + return this + } + /** * @suppress * Internal function diff --git a/src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt b/src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt new file mode 100644 index 00000000..ed788ab7 --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018. Robert Jaros + */ +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.ListTag +import pl.treksoft.kvision.html.ListType +import pl.treksoft.kvision.panel.Root +import pl.treksoft.kvision.utils.px + +/** + * Context menu component. + * + * @constructor + * @param classes a set of CSS class names + */ +open class ContextMenu( + classes: Set = setOf(), init: (ContextMenu.() -> Unit)? = null +) : ListTag(ListType.UL, classes = classes + "dropdown-menu") { + + init { + @Suppress("LeakingThis") + hide() + @Suppress("LeakingThis") + display = Display.BLOCK + val root = 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 { + this.top = mouseEvent.pageY.toInt().px + this.left = mouseEvent.pageX.toInt().px + this.show() + return this + } + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun Widget.contextMenu( + classes: Set = setOf(), init: (ContextMenu.() -> Unit)? = null + ): ContextMenu { + val contextMenu = ContextMenu(classes).apply { init?.invoke(this) } + this.setContextMenu(contextMenu) + return contextMenu + } + } +} diff --git a/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt b/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt index 9a349707..4622ceca 100644 --- a/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt +++ b/src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt @@ -186,15 +186,11 @@ open class DropDown( elements?.let { elems -> val c = elems.map { when (it.second) { - DD.HEADER.option -> Tag(TAG.LI, it.first, classes = setOf("dropdown-header")) - DD.SEPARATOR.option -> { - val tag = Tag(TAG.LI, it.first, classes = setOf("divider")) - tag.role = "separator" - tag - } + DD.HEADER.option -> Header(it.first) + DD.SEPARATOR.option -> Separator() DD.DISABLED.option -> { val tag = Tag(TAG.LI, classes = setOf("disabled")) - tag.add(Link(it.first, "#")) + tag.add(Link(it.first, "javascript:void(0)")) tag } else -> Link(it.first, it.second) @@ -246,10 +242,11 @@ open class DropDown( */ fun Container.dropDown( text: String, elements: List? = null, icon: String? = null, - style: ButtonStyle = ButtonStyle.DEFAULT, disabled: Boolean = false, navbar: Boolean = false, + style: ButtonStyle = ButtonStyle.DEFAULT, disabled: Boolean = false, forNavbar: Boolean = false, classes: Set = setOf(), init: (DropDown.() -> Unit)? = null ): DropDown { - val dropDown = DropDown(text, elements, icon, style, disabled, navbar, classes).apply { init?.invoke(this) } + val dropDown = + DropDown(text, elements, icon, style, disabled, forNavbar, classes).apply { init?.invoke(this) } this.add(dropDown) return dropDown } @@ -264,6 +261,11 @@ internal class DropDownButton( init { this.id = id + setInternalEventListener { + click = { e -> + if (parent?.parent is ContextMenu) e.asDynamic().dropDownCM = true + } + } } override fun render(): VNode { diff --git a/src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt b/src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt new file mode 100644 index 00000000..e09d0246 --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2018. Robert Jaros + */ +package pl.treksoft.kvision.dropdown + +import pl.treksoft.kvision.html.ListTag +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 = setOf()) : + Tag(TAG.LI, content, classes = classes + "dropdown-header") { + + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun ListTag.header(content: String? = null, classes: Set = 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 = setOf()): Header { + val header = Header(content, classes) + this.add(header) + return header + } + } +} diff --git a/src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt b/src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt new file mode 100644 index 00000000..e786b47a --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018. Robert Jaros + */ +package pl.treksoft.kvision.dropdown + +import pl.treksoft.kvision.core.StringPair +import pl.treksoft.kvision.html.ListTag +import pl.treksoft.kvision.html.TAG +import pl.treksoft.kvision.html.Tag + +/** + * Menu separator component. + * + * @constructor + * @param classes a set of CSS class names + */ +open class Separator(classes: Set = setOf()) : Tag(TAG.LI, classes = classes + "divider") { + + override fun getSnAttrs(): List { + val pr = super.getSnAttrs().toMutableList() + pr.add("role" to "separator") + return pr + } + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun ListTag.separator(classes: Set = 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 = setOf()): Separator { + val separator = Separator(classes) + this.add(separator) + return separator + } + } +} diff --git a/src/main/kotlin/pl/treksoft/kvision/html/Link.kt b/src/main/kotlin/pl/treksoft/kvision/html/Link.kt index 94c7c594..50a45047 100644 --- a/src/main/kotlin/pl/treksoft/kvision/html/Link.kt +++ b/src/main/kotlin/pl/treksoft/kvision/html/Link.kt @@ -25,6 +25,7 @@ import com.github.snabbdom.VNode import pl.treksoft.kvision.core.Container import pl.treksoft.kvision.core.ResString import pl.treksoft.kvision.core.StringPair +import pl.treksoft.kvision.dropdown.DropDown import pl.treksoft.kvision.panel.SimplePanel /** @@ -81,5 +82,37 @@ open class Link( 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 ListTag.linkDisabled( + label: String, icon: String? = null, image: ResString? = null, + classes: Set = setOf(), init: (Link.() -> Unit)? = null + ): Link { + val link = Link(label, "javascript:void(0)", icon, image, classes).apply { init?.invoke(this) } + val tag = Tag(TAG.LI, classes = setOf("disabled")) + tag.add(link) + this.add(tag) + 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.linkDisabled( + label: String, icon: String? = null, image: ResString? = null, + classes: Set = setOf(), init: (Link.() -> Unit)? = null + ): Link { + val link = Link(label, "javascript:void(0)", icon, image, classes).apply { init?.invoke(this) } + val tag = Tag(TAG.LI, classes = setOf("disabled")) + tag.add(link) + this.add(tag) + return link + } } } diff --git a/src/main/kotlin/pl/treksoft/kvision/html/List.kt b/src/main/kotlin/pl/treksoft/kvision/html/List.kt index fb49cd62..020806ce 100644 --- a/src/main/kotlin/pl/treksoft/kvision/html/List.kt +++ b/src/main/kotlin/pl/treksoft/kvision/html/List.kt @@ -27,6 +27,7 @@ import pl.treksoft.kvision.KVManager import pl.treksoft.kvision.core.Component import pl.treksoft.kvision.core.Container import pl.treksoft.kvision.core.StringBoolPair +import pl.treksoft.kvision.dropdown.DropDown import pl.treksoft.kvision.panel.SimplePanel /** @@ -96,14 +97,14 @@ open class ListTag( val childrenElements = children.filter { it.visible } val res = when (type) { ListType.UL, ListType.OL, ListType.UNSTYLED, ListType.INLINE -> childrenElements.map { v -> - if (v is Tag && v.type == TAG.LI) { + if (v is Tag && v.type == TAG.LI || v is DropDown && v.forNavbar) { v.renderVNode() } else { h("li", arrayOf(v.renderVNode())) } } ListType.DL, ListType.DL_HORIZ -> childrenElements.mapIndexed { index, v -> - if (v is Tag && v.type == TAG.LI) { + if (v is Tag && v.type == TAG.LI || v is DropDown && v.forNavbar) { v.renderVNode() } else { h(if (index % 2 == 0) "dt" else "dd", arrayOf(v.renderVNode())) diff --git a/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt b/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt index a51191d4..7ab68fdd 100644 --- a/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt +++ b/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt @@ -25,6 +25,7 @@ import com.github.snabbdom.VNode import com.github.snabbdom.h import pl.treksoft.kvision.KVManager import pl.treksoft.kvision.core.StringBoolPair +import pl.treksoft.kvision.dropdown.ContextMenu import pl.treksoft.kvision.modal.Modal import pl.treksoft.kvision.utils.snClasses import pl.treksoft.kvision.utils.snOpt @@ -44,6 +45,7 @@ import pl.treksoft.kvision.utils.snOpt */ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Unit)? = null) : SimplePanel() { private val modals: MutableList = mutableListOf() + private val contextMenus: MutableList = mutableListOf() private var rootVnode: VNode = renderVNode() internal var renderDisabled = false @@ -60,9 +62,9 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni return if (!fixed) { render("div#$id", arrayOf(h("div", snOpt { `class` = snClasses(listOf("row" to true)) - }, childrenVNodes() + modalsVNodes()))) + }, childrenVNodes() + modalsVNodes() + contextMenusVNodes()))) } else { - render("div#$id", childrenVNodes() + modalsVNodes()) + render("div#$id", childrenVNodes() + modalsVNodes() + contextMenusVNodes()) } } @@ -72,10 +74,26 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni refresh() } + internal fun addContextMenu(contextMenu: ContextMenu) { + contextMenus.add(contextMenu) + contextMenu.parent = this + this.setInternalEventListener { + click = { e -> + @Suppress("UnsafeCastFromDynamic") + if (!e.asDynamic().dropDownCM) contextMenu.hide() + } + } + refresh() + } + private fun modalsVNodes(): Array { return modals.filter { it.visible }.map { it.renderVNode() }.toTypedArray() } + private fun contextMenusVNodes(): Array { + return contextMenus.filter { it.visible }.map { it.renderVNode() }.toTypedArray() + } + override fun getSnClass(): List { val css = if (!fixed) "container-fluid" else "container" return super.getSnClass() + (css to true) -- cgit