diff options
Diffstat (limited to 'src/main/kotlin/pl/treksoft/kvision')
18 files changed, 601 insertions, 114 deletions
diff --git a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt index faa0b77c..d1a4a8be 100644 --- a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt +++ b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt @@ -29,6 +29,7 @@ import com.github.snabbdom.datasetModule import com.github.snabbdom.eventListenersModule import com.github.snabbdom.propsModule import com.github.snabbdom.styleModule +import org.w3c.dom.HTMLElement import pl.treksoft.kvision.core.Component import pl.treksoft.kvision.utils.isIE11 import kotlin.browser.document @@ -72,6 +73,10 @@ internal object KVManager { return sdPatch(container, vnode) } + internal fun patch(element: HTMLElement, vnode: VNode): VNode { + return sdPatch(element, vnode) + } + internal fun patch(oldVNode: VNode, newVNode: VNode): VNode { return sdPatch(oldVNode, newVNode) } diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Component.kt b/src/main/kotlin/pl/treksoft/kvision/core/Component.kt index 411eae8d..fe5569d4 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Component.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Component.kt @@ -97,6 +97,26 @@ interface Component { fun removeSurroundingCssClass(css: Style): Component /** + * Returns the value of an additional attribute. + * @param name the name of the attribute + * @return the value of the attribute + */ + fun getAttribute(name: String): String? + + /** + * Sets the value of additional attribute. + * @param name the name of the attribute + * @param value the value of the attribute + */ + fun setAttribute(name: String, value: String): Component + + /** + * Removes the value of additional attribute. + * @param name the name of the attribute + */ + fun removeAttribute(name: String): Component + + /** * @suppress * Internal function * Renders current component as a Snabbdom vnode. diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Style.kt b/src/main/kotlin/pl/treksoft/kvision/core/Style.kt index 282d2e7e..ff91c429 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Style.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Style.kt @@ -21,11 +21,6 @@ */ package pl.treksoft.kvision.core -import com.github.snabbdom.VNode -import com.github.snabbdom.h -import org.w3c.dom.Node -import pl.treksoft.jquery.JQuery -import pl.treksoft.kvision.panel.Root import kotlin.reflect.KProperty /** @@ -41,8 +36,6 @@ open class Style(className: String? = null, parentStyle: Style? = null, init: (S StyledComponent() { private val propertyValues: MutableMap<String, Any?> = mutableMapOf() - override var parent: Container? = Root.getFirstRoot() - private val newClassName: String = if (parentStyle == null) { className ?: "kv_styleclass_${counter++}" } else { @@ -55,86 +48,19 @@ open class Style(className: String? = null, parentStyle: Style? = null, init: (S var className: String by refreshOnUpdate(newClassName) init { + @Suppress("LeakingThis") styles.add(this) @Suppress("LeakingThis") init?.invoke(this) } - override var visible: Boolean = true - set(value) { - val oldField = field - field = value - if (oldField != field) refresh() - } - - override fun addCssClass(css: String): Component { - return this - } - - override fun removeCssClass(css: String): Component { - return this - } - - override fun addSurroundingCssClass(css: String): Component { - return this - } - - override fun removeSurroundingCssClass(css: String): Component { - return this - } - - override fun addCssClass(css: Style): Component { - return this - } - - override fun removeCssClass(css: Style): Component { - return this - } - - override fun addSurroundingCssClass(css: Style): Component { - return this - } - - override fun removeSurroundingCssClass(css: Style): Component { - return this - } - - override fun renderVNode(): VNode { - return h("style", arrayOf(generateStyle())) - } - internal fun generateStyle(): String { - val styles = getSnStyle() + val styles = getSnStyleInternal() return ".$className {\n" + styles.joinToString("\n") { "${it.first}: ${it.second};" } + "\n}" } - override fun getElement(): Node? { - return null - } - - override fun getElementJQuery(): JQuery? { - return null - } - - override fun getElementJQueryD(): dynamic { - return null - } - - override fun clearParent(): Component { - this.parent = null - return this - } - - override fun getRoot(): Root? { - return this.parent?.getRoot() - } - - override fun dispose() { - styles.remove(this) - } - protected fun <T> refreshOnUpdate(refreshFunction: ((T) -> Unit) = { this.refresh() }) = RefreshDelegateProvider<T>(null, refreshFunction) diff --git a/src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt b/src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt index aa3f26bb..d9fa63fc 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/StyledComponent.kt @@ -29,7 +29,7 @@ import kotlin.reflect.KProperty * Base class for components supporting CSS styling. */ @Suppress("LargeClass") -abstract class StyledComponent : Component { +abstract class StyledComponent { private val propertyValues: MutableMap<String, Any?> = mutableMapOf() /** diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt index 3388a011..fbcd89da 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt @@ -53,11 +53,12 @@ import kotlin.reflect.KProperty * @param classes Set of CSS class names */ @Suppress("TooManyFunctions", "LargeClass") -open class Widget(classes: Set<String> = setOf()) : StyledComponent() { +open class Widget(classes: Set<String> = setOf()) : StyledComponent(), Component { private val propertyValues: MutableMap<String, Any?> = mutableMapOf() internal val classes = classes.toMutableSet() internal val surroundingClasses: MutableSet<String> = mutableSetOf() + internal val attributes: MutableMap<String, String> = mutableMapOf() internal val internalListeners = mutableListOf<SnOn<Widget>.() -> Unit>() internal val listeners = mutableListOf<SnOn<Widget>.() -> Unit>() @@ -243,6 +244,9 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() { if (draggable == true) { snattrs.add("draggable" to "true") } + if (attributes.isNotEmpty()) { + snattrs += attributes.toList() + } return snattrs } @@ -565,6 +569,22 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() { return removeSurroundingCssClass(css.className) } + override fun getAttribute(name: String): String? { + return this.attributes[name] + } + + override fun setAttribute(name: String, value: String): Widget { + this.attributes[name] = value + refresh() + return this + } + + override fun removeAttribute(name: String): Widget { + this.attributes.remove(name) + refresh() + return this + } + override fun getElement(): Node? { return this.vnode?.elm } diff --git a/src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt b/src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt index 57ce88eb..3019fb6f 100644 --- a/src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt +++ b/src/main/kotlin/pl/treksoft/kvision/form/FormControl.kt @@ -47,6 +47,16 @@ interface FormInput : Component { * The name attribute of the generated HTML input element. */ var name: String? + + /** + * Makes the input element focused. + */ + fun focus() + + /** + * Makes the input element blur. + */ + fun blur() } /** diff --git a/src/main/kotlin/pl/treksoft/kvision/form/check/CheckInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/check/CheckInput.kt index 6973efe9..07b86b4c 100644 --- a/src/main/kotlin/pl/treksoft/kvision/form/check/CheckInput.kt +++ b/src/main/kotlin/pl/treksoft/kvision/form/check/CheckInput.kt @@ -152,14 +152,14 @@ abstract class CheckInput( /** * Makes the input element focused. */ - open fun focus() { + override fun focus() { getElementJQuery()?.focus() } /** * Makes the input element blur. */ - open fun blur() { + override fun blur() { getElementJQuery()?.blur() } diff --git a/src/main/kotlin/pl/treksoft/kvision/form/check/RadioGroupInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/check/RadioGroupInput.kt index 98839982..fca681f6 100644 --- a/src/main/kotlin/pl/treksoft/kvision/form/check/RadioGroupInput.kt +++ b/src/main/kotlin/pl/treksoft/kvision/form/check/RadioGroupInput.kt @@ -150,11 +150,11 @@ open class RadioGroupInput( } } - fun focus() { + override fun focus() { getChildren().filterIsInstance<Radio>().firstOrNull()?.focus() } - fun blur() { + override fun blur() { getChildren().filterIsInstance<Radio>().firstOrNull()?.blur() } diff --git a/src/main/kotlin/pl/treksoft/kvision/form/select/SimpleSelect.kt b/src/main/kotlin/pl/treksoft/kvision/form/select/SimpleSelect.kt new file mode 100644 index 00000000..4d278ad2 --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/form/select/SimpleSelect.kt @@ -0,0 +1,209 @@ +/* + * 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.form.select + +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.form.FieldLabel +import pl.treksoft.kvision.form.HelpBlock +import pl.treksoft.kvision.form.StringFormControl +import pl.treksoft.kvision.panel.SimplePanel +import pl.treksoft.kvision.utils.SnOn + +/** + * The form field component for SimpleSelect control. + * + * @constructor + * @param options an optional list of options (value to label pairs) for the select control + * @param value selected value + * @param emptyOption determines if an empty option is automatically generated + * @param name the name attribute of the generated HTML input element + * @param label label text bound to the input element + * @param rich determines if [label] can contain HTML code + */ +@Suppress("TooManyFunctions") +open class SimpleSelect( + options: List<StringPair>? = null, value: String? = null, emptyOption: Boolean = false, + name: String? = null, label: String? = null, rich: Boolean = false +) : SimplePanel(setOf("form-group")), StringFormControl { + + /** + * A list of options (value to label pairs) for the select control. + */ + var options + get() = input.options + set(value) { + input.options = value + } + /** + * A value of the selected option. + */ + override var value + get() = input.value + set(value) { + input.value = value + } + /** + * The value of the selected child option. + * + * This value is placed directly in the generated HTML code, while the [value] property is dynamically + * bound to the select component. + */ + var startValue + get() = input.startValue + set(value) { + input.startValue = value + } + /** + * Determines if an empty option is automatically generated. + */ + var emptyOption + get() = input.emptyOption + set(value) { + input.emptyOption = value + } + /** + * Determines if the select is automatically focused. + */ + var autofocus + get() = input.autofocus + set(value) { + input.autofocus = value + } + /** + * The label text bound to the select element. + */ + var label + get() = flabel.content + set(value) { + flabel.content = value + } + /** + * Determines if [label] can contain HTML code. + */ + var rich + get() = flabel.rich + set(value) { + flabel.rich = value + } + + private val idc = "kv_form_simpleselect_$counter" + final override val input: SimpleSelectInput = SimpleSelectInput( + options, value, emptyOption, setOf("form-control") + ).apply { + this.id = idc + this.name = name + } + final override val flabel: FieldLabel = FieldLabel(idc, label, rich) + final override val validationInfo: HelpBlock = HelpBlock().apply { visible = false } + + init { + @Suppress("LeakingThis") + input.eventTarget = this + this.addInternal(flabel) + this.addInternal(input) + this.addInternal(validationInfo) + counter++ + } + + override fun getSnClass(): List<StringBoolPair> { + val cl = super.getSnClass().toMutableList() + if (validatorError != null) { + cl.add("has-error" to true) + } + return cl + } + + @Suppress("UNCHECKED_CAST") + override fun <T : Widget> setEventListener(block: SnOn<T>.() -> Unit): Widget { + input.setEventListener(block) + return this + } + + override fun setEventListener(block: SnOn<Widget>.() -> Unit): Widget { + input.setEventListener(block) + return this + } + + override fun removeEventListeners(): Widget { + input.removeEventListeners() + return this + } + + override fun add(child: Component): SimplePanel { + input.add(child) + return this + } + + override fun addAll(children: List<Component>): SimplePanel { + input.addAll(children) + return this + } + + override fun remove(child: Component): SimplePanel { + input.remove(child) + return this + } + + override fun removeAll(): SimplePanel { + input.removeAll() + return this + } + + override fun getChildren(): List<Component> { + return input.getChildren() + } + + override fun focus() { + input.focus() + } + + override fun blur() { + input.blur() + } + + companion object { + internal var counter = 0 + + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun Container.simpleSelect( + options: List<StringPair>? = null, + value: String? = null, + emptyOption: Boolean = false, + name: String? = null, + label: String? = null, + rich: Boolean = false, + init: (SimpleSelect.() -> Unit)? = null + ): SimpleSelect { + val simpleSelect = SimpleSelect(options, value, emptyOption, name, label, rich).apply { init?.invoke(this) } + this.add(simpleSelect) + return simpleSelect + } + } +} diff --git a/src/main/kotlin/pl/treksoft/kvision/form/select/SimpleSelectInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/select/SimpleSelectInput.kt new file mode 100644 index 00000000..df334c1c --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/form/select/SimpleSelectInput.kt @@ -0,0 +1,213 @@ +/* + * 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.form.select + +import com.github.snabbdom.VNode +import pl.treksoft.kvision.core.Container +import pl.treksoft.kvision.core.StringBoolPair +import pl.treksoft.kvision.core.StringPair +import pl.treksoft.kvision.form.FormInput +import pl.treksoft.kvision.form.InputSize +import pl.treksoft.kvision.html.TAG +import pl.treksoft.kvision.html.Tag +import pl.treksoft.kvision.panel.SimplePanel + +internal const val KVNULL = "#kvnull" + +/** + * Simple select component. + * + * @constructor + * @param options an optional list of options (value to label pairs) for the select control + * @param value text input value + * @param emptyOption determines if an empty option is automatically generated + * @param classes a set of CSS class names + */ +open class SimpleSelectInput( + options: List<StringPair>? = null, value: String? = null, emptyOption: Boolean = false, + classes: Set<String> = setOf() +) : SimplePanel(classes + "form-control"), FormInput { + + /** + * A list of options (value to label pairs) for the select control. + */ + var options by refreshOnUpdate(options) { setChildrenFromOptions() } + + /** + * Text input value. + */ + var value by refreshOnUpdate(value) { refreshState() } + /** + * The value of the selected child option. + * + * This value is placed directly in the generated HTML code, while the [value] property is dynamically + * bound to the select component. + */ + var startValue by refreshOnUpdate(value) { this.value = it; selectOption() } + /** + * The name attribute of the generated HTML input element. + */ + override var name: String? by refreshOnUpdate() + /** + * Determines if the field is disabled. + */ + override var disabled by refreshOnUpdate(false) + /** + * Determines if the text input is automatically focused. + */ + var autofocus: Boolean? by refreshOnUpdate() + /** + * Determines if an empty option is automatically generated. + */ + var emptyOption by refreshOnUpdate(emptyOption) { setChildrenFromOptions() } + /** + * The size of the input. + */ + override var size: InputSize? by refreshOnUpdate() + + init { + this.vnkey = "kv_simpleselectinput_${counter++}" + setChildrenFromOptions() + this.setInternalEventListener<SimpleSelectInput> { + change = { + self.changeValue() + } + } + } + + override fun render(): VNode { + return render("select", childrenVNodes()) + } + + private fun setChildrenFromOptions() { + super.removeAll() + if (emptyOption) { + super.add(Tag(TAG.OPTION, "", attributes = mapOf("value" to KVNULL))) + } + options?.let { + val c = it.map { + val attributes = if (it.first == value) { + mapOf("value" to it.first, "selected" to "selected") + } else { + mapOf("value" to it.first) + } + Tag(TAG.OPTION, it.second, attributes = attributes) + } + super.addAll(c) + } + } + + private fun selectOption() { + children.forEach { child -> + if (child is Tag && child.type == TAG.OPTION) { + if (value != null && child.getAttribute("value") == value) { + child.setAttribute("selected", "selected") + } else { + child.removeAttribute("selected") + } + } + } + } + + override fun getSnClass(): List<StringBoolPair> { + val cl = super.getSnClass().toMutableList() + size?.let { + cl.add(it.className to true) + } + return cl + } + + override fun getSnAttrs(): List<StringPair> { + val sn = super.getSnAttrs().toMutableList() + name?.let { + sn.add("name" to it) + } + autofocus?.let { + if (it) { + sn.add("autofocus" to "autofocus") + } + } + if (disabled) { + sn.add("disabled" to "disabled") + } + return sn + } + + override fun afterInsert(node: VNode) { + refreshState() + } + + /** + * @suppress + * Internal function + */ + protected open fun refreshState() { + value?.let { + getElementJQuery()?.`val`(it) + } ?: getElementJQueryD()?.`val`(null) + } + + /** + * @suppress + * Internal function + */ + protected open fun changeValue() { + val v = getElementJQuery()?.`val`() as String? + if (v != null && v.isNotEmpty() && v != KVNULL) { + this.value = v + } else { + this.value = null + } + } + + /** + * Makes the input element focused. + */ + override fun focus() { + getElementJQuery()?.focus() + } + + /** + * Makes the input element blur. + */ + override fun blur() { + getElementJQuery()?.blur() + } + + companion object { + internal var counter = 0 + + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun Container.simpleSelectInput( + options: List<StringPair>? = null, value: String? = null, emptyOption: Boolean = false, + classes: Set<String> = setOf(), init: (SimpleSelectInput.() -> Unit)? = null + ): SimpleSelectInput { + val simpleSelectInput = SimpleSelectInput(options, value, emptyOption, classes).apply { init?.invoke(this) } + this.add(simpleSelectInput) + return simpleSelectInput + } + } +} diff --git a/src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt index c9ea2dba..393ae63f 100644 --- a/src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt +++ b/src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt @@ -157,14 +157,14 @@ abstract class AbstractTextInput( /** * Makes the input element focused. */ - open fun focus() { + override fun focus() { getElementJQuery()?.focus() } /** * Makes the input element blur. */ - open fun blur() { + override fun blur() { getElementJQuery()?.blur() } diff --git a/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt b/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt index 32119a90..68e43da0 100644 --- a/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt +++ b/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt @@ -69,6 +69,7 @@ enum class TAG(internal val tagName: String) { SAMP("samp"), SPAN("span"), LI("li"), + HR("hr"), CAPTION("caption"), THEAD("thead"), @@ -78,7 +79,10 @@ enum class TAG(internal val tagName: String) { TD("td"), FORM("form"), - INPUT("input") + INPUT("input"), + SELECT("select"), + OPTION("option"), + BUTTON("button") } /** @@ -101,11 +105,13 @@ enum class Align(val className: String) { * @param rich determines if [content] can contain HTML code * @param align content align * @param classes a set of CSS class names + * @param attributes a map of additional attributes * @param init an initializer extension function */ open class Tag( type: TAG, content: String? = null, rich: Boolean = false, align: Align? = null, - classes: Set<String> = setOf(), init: (Tag.() -> Unit)? = null + classes: Set<String> = setOf(), attributes: Map<String, String> = mapOf(), + init: (Tag.() -> Unit)? = null ) : SimplePanel(classes), Template { /** @@ -139,6 +145,7 @@ open class Tag( override var templates: Map<String, (Any?) -> String> by refreshOnUpdate(mapOf()) init { + this.attributes += attributes @Suppress("LeakingThis") init?.invoke(this) } @@ -187,9 +194,10 @@ open class Tag( */ fun Container.tag( type: TAG, content: String? = null, rich: Boolean = false, align: Align? = null, - classes: Set<String> = setOf(), init: (Tag.() -> Unit)? = null + classes: Set<String> = setOf(), attributes: Map<String, String> = mapOf(), + init: (Tag.() -> Unit)? = null ): Tag { - val tag = Tag(type, content, rich, align, classes, init) + val tag = Tag(type, content, rich, align, classes, attributes, init) this.add(tag) return tag } diff --git a/src/main/kotlin/pl/treksoft/kvision/panel/FlexPanel.kt b/src/main/kotlin/pl/treksoft/kvision/panel/FlexPanel.kt index 310d4d49..010f7cba 100644 --- a/src/main/kotlin/pl/treksoft/kvision/panel/FlexPanel.kt +++ b/src/main/kotlin/pl/treksoft/kvision/panel/FlexPanel.kt @@ -24,7 +24,7 @@ package pl.treksoft.kvision.panel import pl.treksoft.kvision.core.Component import pl.treksoft.kvision.core.Container import pl.treksoft.kvision.core.StringPair -import pl.treksoft.kvision.core.StyledComponent +import pl.treksoft.kvision.core.Widget import pl.treksoft.kvision.core.WidgetWrapper import pl.treksoft.kvision.utils.px @@ -152,10 +152,10 @@ open class FlexPanel( } private fun refreshSpacing() { - getChildren().filterIsInstance<StyledComponent>().map { applySpacing(it) } + getChildren().filterIsInstance<Widget>().map { applySpacing(it) } } - private fun applySpacing(wrapper: StyledComponent): StyledComponent { + private fun applySpacing(wrapper: Widget): Widget { wrapper.marginTop = null wrapper.marginRight = null wrapper.marginBottom = null diff --git a/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt b/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt index c17ea1a4..2d9dcc46 100644 --- a/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt +++ b/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt @@ -23,6 +23,7 @@ package pl.treksoft.kvision.panel import com.github.snabbdom.VNode import com.github.snabbdom.h +import org.w3c.dom.HTMLElement import pl.treksoft.kvision.KVManager import pl.treksoft.kvision.core.StringBoolPair import pl.treksoft.kvision.core.Style @@ -45,7 +46,12 @@ import pl.treksoft.kvision.utils.snOpt * @param init an initializer extension function */ @Suppress("TooManyFunctions") -class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Unit)? = null) : SimplePanel() { +class Root( + id: String? = null, + element: HTMLElement? = null, + private val fixed: Boolean = false, + init: (Root.() -> Unit)? = null +) : SimplePanel() { private val contextMenus: MutableList<ContextMenu> = mutableListOf() private var rootVnode: VNode = renderVNode() @@ -54,11 +60,17 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni val isFirstRoot = roots.isEmpty() init { - rootVnode = KVManager.patch(id, this.renderVNode()) - this.id = id + if (id != null) { + rootVnode = KVManager.patch(id, this.renderVNode()) + this.id = id + } else if (element != null) { + rootVnode = KVManager.patch(element, this.renderVNode()) + this.id = "kv_root_${counter++}" + } else { + throw IllegalArgumentException("No root element specified!") + } roots.add(this) if (isFirstRoot) { - Style.styles.forEach { it.parent = this } Modal.modals.forEach { it.parent = this } } @Suppress("LeakingThis") @@ -89,9 +101,8 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni private fun stylesVNodes(): Array<VNode> { return if (isFirstRoot) { - val visibleStyles = Style.styles.filter { it.visible } - if (visibleStyles.isNotEmpty()) { - val stylesDesc = visibleStyles.joinToString("\n") { it.generateStyle() } + if (Style.styles.isNotEmpty()) { + val stylesDesc = Style.styles.joinToString("\n") { it.generateStyle() } arrayOf(h("style", arrayOf(stylesDesc))) } else { arrayOf() @@ -144,6 +155,8 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni } companion object { + internal var counter = 0 + /** * @suppress internal function */ diff --git a/src/main/kotlin/pl/treksoft/kvision/panel/StackPanel.kt b/src/main/kotlin/pl/treksoft/kvision/panel/StackPanel.kt index 3b045fa6..37dd449b 100644 --- a/src/main/kotlin/pl/treksoft/kvision/panel/StackPanel.kt +++ b/src/main/kotlin/pl/treksoft/kvision/panel/StackPanel.kt @@ -55,6 +55,8 @@ open class StackPanel( activeIndex = children.indexOf(value) } + internal val childrenMap = mutableMapOf<Int, Component>() + init { @Suppress("LeakingThis") init?.invoke(this) @@ -76,8 +78,11 @@ open class StackPanel( */ open fun add(panel: Component, route: String): StackPanel { add(panel) - val currentIndex = children.size - 1 - routing.on(route, { _ -> activeIndex = currentIndex }).resolve() + val currentIndex = counter++ + childrenMap[currentIndex] = panel + routing.on(route, { _ -> + activeChild = childrenMap[currentIndex]!! + }).resolve() return this } @@ -97,17 +102,23 @@ open class StackPanel( override fun remove(child: Component): StackPanel { super.remove(child) + childrenMap.filter { it.value == child }.keys.firstOrNull()?.let { + childrenMap.remove(it) + } if (activeIndex > children.size - 1) activeIndex = children.size - 1 return this } override fun removeAll(): StackPanel { super.removeAll() + childrenMap.clear() if (activeIndex > children.size - 1) activeIndex = children.size - 1 return this } companion object { + internal var counter = 0 + /** * DSL builder extension function. * diff --git a/src/main/kotlin/pl/treksoft/kvision/panel/TabPanel.kt b/src/main/kotlin/pl/treksoft/kvision/panel/TabPanel.kt index 5a4fac0c..ae8360b5 100644 --- a/src/main/kotlin/pl/treksoft/kvision/panel/TabPanel.kt +++ b/src/main/kotlin/pl/treksoft/kvision/panel/TabPanel.kt @@ -25,10 +25,13 @@ 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.Link +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. @@ -95,6 +98,8 @@ open class TabPanel( private var nav = Tag(TAG.UL, classes = navClasses) private var content = StackPanel(false) + internal val childrenMap = mutableMapOf<Int, Component>() + init { when (tabPosition) { TabPosition.TOP -> { @@ -135,23 +140,46 @@ open class TabPanel( * @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, route: String? = null + image: ResString? = null, closable: Boolean = false, route: String? = null ): TabPanel { - val tag = Tag(TAG.LI) - tag.role = "presentation" - tag.add(Link(title, "#", icon, image)) - val index = nav.children.size - tag.setEventListener { - click = { e -> - activeIndex = index - e.preventDefault() - if (route != null) { - routing.navigate(route) + val currentIndex = counter++ + childrenMap[currentIndex] = panel + val tag = Tag(TAG.LI) { + role = "presentation" + link(title, "#", icon, image) { + if (closable) { + cicon("remove") { + addCssClass("kv-tab-close") + setEventListener<Icon> { + click = { e -> + val actIndex = this@TabPanel.content.children.indexOf(childrenMap[currentIndex]) + e.asDynamic().data = actIndex + 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.children.indexOf(childrenMap[currentIndex]) + e.preventDefault() + if (route != null) { + routing.navigate(route) + } } } } @@ -162,7 +190,8 @@ open class TabPanel( } content.add(panel) if (route != null) { - routing.on(route, { _ -> activeIndex = index }).resolve() + routing.on(route, { _ -> activeIndex = this@TabPanel.content.children.indexOf(childrenMap[currentIndex]) }) + .resolve() } return this } @@ -172,6 +201,9 @@ open class TabPanel( */ open fun removeTab(index: Int): TabPanel { nav.remove(nav.children[index]) + childrenMap.filter { it.value == content.children[index] }.keys.firstOrNull()?.let { + childrenMap.remove(it) + } content.remove(content.children[index]) activeIndex = content.activeIndex return this @@ -191,14 +223,32 @@ open class TabPanel( return removeTab(index) } + /** + * Returns child component by tab index. + * @param index tab index + */ + open fun getChildComponent(index: Int): Component? { + return content.children[index] + } + + /** + * Returns tab header component by tab index. + * @param index tab index + */ + open fun getNavComponent(index: Int): Tag? { + return nav.children[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. * diff --git a/src/main/kotlin/pl/treksoft/kvision/routing/Routing.kt b/src/main/kotlin/pl/treksoft/kvision/routing/Routing.kt index 950dfe1c..4628d989 100644 --- a/src/main/kotlin/pl/treksoft/kvision/routing/Routing.kt +++ b/src/main/kotlin/pl/treksoft/kvision/routing/Routing.kt @@ -48,4 +48,4 @@ open class Routing : Navigo(null, true, "#!") { /** * Default JavaScript router. */ -var routing = Routing() +var routing = Routing().also { it.resolve() } diff --git a/src/main/kotlin/pl/treksoft/kvision/utils/Snabbdom.kt b/src/main/kotlin/pl/treksoft/kvision/utils/Snabbdom.kt index d2177a97..cb48cfd1 100644 --- a/src/main/kotlin/pl/treksoft/kvision/utils/Snabbdom.kt +++ b/src/main/kotlin/pl/treksoft/kvision/utils/Snabbdom.kt @@ -130,6 +130,8 @@ interface BtOn : On { var tabulatorDataLoading: ((KvEvent) -> Unit)? var tabulatorDataLoaded: ((KvEvent) -> Unit)? var tabulatorDataEdited: ((KvEvent) -> Unit)? + var tabClosing: ((KvEvent) -> Unit)? + var tabClosed: ((KvEvent) -> Unit)? } /** |