aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Jaros <rjaros@finn.pl>2018-03-22 20:23:54 +0100
committerRobert Jaros <rjaros@finn.pl>2018-03-22 20:23:54 +0100
commit7081cfaed23fe8b34bfdf15918775a846d7649e0 (patch)
tree15458e3d7fb2cc0ebf32281362ba44f82d27fa0b
parent6ba1eefc59a2940a7258655da4e88b4118e61746 (diff)
downloadkvision-7081cfaed23fe8b34bfdf15918775a846d7649e0.tar.gz
kvision-7081cfaed23fe8b34bfdf15918775a846d7649e0.tar.bz2
kvision-7081cfaed23fe8b34bfdf15918775a846d7649e0.zip
Context menu component based on Bootstrap dropdown.
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Widget.kt19
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/dropdown/ContextMenu.kt66
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/dropdown/DropDown.kt20
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/dropdown/Header.kt44
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/dropdown/Separator.kt48
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/Link.kt33
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/List.kt5
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/panel/Root.kt22
8 files changed, 243 insertions, 14 deletions
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<String> = setOf()) : StyledComponent() {
internal val internalListeners = mutableListOf<SnOn<Widget>.() -> Unit>()
internal val listeners = mutableListOf<SnOn<Widget>.() -> Unit>()
- override var parent: Component? = null
+ override var parent: Container? = null
override var visible: Boolean = true
set(value) {
@@ -542,6 +544,21 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() {
}
/**
+ * Sets context menu for the current widget.
+ * @param contextMenu a context menu
+ * @return current widget
+ */
+ open fun setContextMenu(contextMenu: ContextMenu): Widget {
+ setEventListener<Widget> {
+ 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<String> = 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<String> = 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<StringPair>? = 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<String> = 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<DropDownButton> {
+ 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<String> = 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<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/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<String> = setOf()) : Tag(TAG.LI, classes = classes + "divider") {
+
+ override fun getSnAttrs(): List<StringPair> {
+ 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<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/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<String> = 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<String> = 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<Modal> = mutableListOf()
+ private val contextMenus: MutableList<ContextMenu> = 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<Root> {
+ click = { e ->
+ @Suppress("UnsafeCastFromDynamic")
+ if (!e.asDynamic().dropDownCM) contextMenu.hide()
+ }
+ }
+ refresh()
+ }
+
private fun modalsVNodes(): Array<VNode> {
return modals.filter { it.visible }.map { it.renderVNode() }.toTypedArray()
}
+ private fun contextMenusVNodes(): Array<VNode> {
+ return contextMenus.filter { it.visible }.map { it.renderVNode() }.toTypedArray()
+ }
+
override fun getSnClass(): List<StringBoolPair> {
val css = if (!fixed) "container-fluid" else "container"
return super.getSnClass() + (css to true)