From a54a54cef9035293f0bc55464cd1a96e5db128b9 Mon Sep 17 00:00:00 2001 From: Robert Jaros Date: Sun, 2 Feb 2020 01:12:45 +0100 Subject: Add RangeInput and Range components (sliders) (#132) --- .../kotlin/pl/treksoft/kvision/form/range/Range.kt | 249 +++++++++++++++++++++ .../pl/treksoft/kvision/form/range/RangeInput.kt | 228 +++++++++++++++++++ .../treksoft/kvision/form/range/RangeInputSpec.kt | 51 +++++ .../pl/treksoft/kvision/form/range/RangeSpec.kt | 51 +++++ 4 files changed, 579 insertions(+) create mode 100644 src/main/kotlin/pl/treksoft/kvision/form/range/Range.kt create mode 100644 src/main/kotlin/pl/treksoft/kvision/form/range/RangeInput.kt create mode 100644 src/test/kotlin/test/pl/treksoft/kvision/form/range/RangeInputSpec.kt create mode 100644 src/test/kotlin/test/pl/treksoft/kvision/form/range/RangeSpec.kt diff --git a/src/main/kotlin/pl/treksoft/kvision/form/range/Range.kt b/src/main/kotlin/pl/treksoft/kvision/form/range/Range.kt new file mode 100644 index 00000000..ac772d02 --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/form/range/Range.kt @@ -0,0 +1,249 @@ +/* + * 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.range + +import pl.treksoft.kvision.core.Container +import pl.treksoft.kvision.core.StringBoolPair +import pl.treksoft.kvision.core.Widget +import pl.treksoft.kvision.form.FieldLabel +import pl.treksoft.kvision.form.FormHorizontalRatio +import pl.treksoft.kvision.form.InvalidFeedback +import pl.treksoft.kvision.form.NumberFormControl +import pl.treksoft.kvision.panel.SimplePanel +import pl.treksoft.kvision.utils.SnOn + +/** + * The form field component for range input control. + * + * @constructor + * @param value range input value + * @param name the name attribute of the generated HTML input element + * @param min minimal value (default 0) + * @param max maximal value (default 100) + * @param step step value (default 1) + * @param label label text bound to the input element + * @param rich determines if [label] can contain HTML code + */ +open class Range( + value: Number? = null, name: String? = null, min: Number = 0, max: Number = 100, step: Number = DEFAULT_STEP, + label: String? = null, rich: Boolean = false +) : SimplePanel(setOf("form-group")), NumberFormControl { + + /** + * Range input value. + */ + override var value + get() = input.value + set(value) { + input.value = value + } + /** + * The value attribute of the generated HTML input element. + * + * This value is placed directly in generated HTML code, while the [value] property is dynamically + * bound to the range input value. + */ + var startValue + get() = input.startValue + set(value) { + input.startValue = value + } + /** + * Minimal value. + */ + var min + get() = input.min + set(value) { + input.min = value + } + /** + * Maximal value. + */ + var max + get() = input.max + set(value) { + input.max = value + } + /** + * Step value. + */ + var step + get() = input.step + set(value) { + input.step = value + } + /** + * Determines if the text input is automatically focused. + */ + var autofocus + get() = input.autofocus + set(value) { + input.autofocus = value + } + /** + * Determines if the range input is read-only. + */ + var readonly + get() = input.readonly + set(value) { + input.readonly = value + } + /** + * The label text bound to the range input 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 + } + + override var validatorError: String? + get() = super.validatorError + set(value) { + super.validatorError = value + if (value != null) { + input.addSurroundingCssClass("is-invalid") + } else { + input.removeSurroundingCssClass("is-invalid") + } + } + + protected val idc = "kv_form_range_$counter" + final override val input: RangeInput = RangeInput(value, min, max, step).apply { + this.id = idc + this.name = name + } + final override val flabel: FieldLabel = FieldLabel(idc, label, rich) + final override val invalidFeedback: InvalidFeedback = InvalidFeedback().apply { visible = false } + + init { + @Suppress("LeakingThis") + input.eventTarget = this + this.addInternal(flabel) + this.addInternal(input) + this.addInternal(invalidFeedback) + counter++ + } + + override fun getSnClass(): List { + val cl = super.getSnClass().toMutableList() + if (validatorError != null) { + cl.add("text-danger" to true) + } + return cl + } + + @Suppress("UNCHECKED_CAST") + override fun setEventListener(block: SnOn.() -> Unit): Widget { + input.setEventListener(block) + return this + } + + @Deprecated( + "Use onEvent extension function instead.", + ReplaceWith("onEvent(block)", "pl.treksoft.kvision.core.onEvent") + ) + override fun setEventListener(block: SnOn.() -> Unit): Widget { + @Suppress("DEPRECATION") + input.setEventListener(block) + return this + } + + override fun removeEventListeners(): Widget { + input.removeEventListeners() + return this + } + + override fun getValueAsString(): String? { + return input.getValueAsString() + } + + /** + * Change value in plus. + */ + open fun stepUp(): Range { + input.stepUp() + return this + } + + /** + * Change value in minus. + */ + open fun stepDown(): Range { + input.stepDown() + return this + } + + override fun focus() { + input.focus() + } + + override fun blur() { + input.blur() + } + + override fun styleForHorizontalFormPanel(horizontalRatio: FormHorizontalRatio) { + addCssClass("row") + flabel.addCssClass("col-sm-${horizontalRatio.labels}") + flabel.addCssClass("col-form-label") + input.addSurroundingCssClass("col-sm-${horizontalRatio.fields}") + invalidFeedback.addCssClass("offset-sm-${horizontalRatio.labels}") + invalidFeedback.addCssClass("col-sm-${horizontalRatio.fields}") + } + + companion object { + internal var counter = 0 + } +} + +/** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ +fun Container.range( + value: Number? = null, + name: String? = null, + min: Number = 0, + max: Number = 100, + step: Number = DEFAULT_STEP, + label: String? = null, + rich: Boolean = false, + init: (Range.() -> Unit)? = null +): Range { + val range = + Range(value, name, min, max, step, label, rich).apply { + init?.invoke( + this + ) + } + this.add(range) + return range +} diff --git a/src/main/kotlin/pl/treksoft/kvision/form/range/RangeInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/range/RangeInput.kt new file mode 100644 index 00000000..5ad854d0 --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/form/range/RangeInput.kt @@ -0,0 +1,228 @@ +/* + * 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.range + +import com.github.snabbdom.VNode +import org.w3c.dom.HTMLInputElement +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.FormInput +import pl.treksoft.kvision.form.InputSize +import pl.treksoft.kvision.form.ValidationStatus + +internal const val DEFAULT_STEP = 1 + +/** + * Range input component. + * + * @constructor + * @param value range input value + * @param min minimal value (default 0) + * @param max maximal value (default 100) + * @param step step value (default 1) + * @param classes a set of CSS class names + */ +open class RangeInput( + value: Number? = null, min: Number = 0, max: Number = 100, step: Number = DEFAULT_STEP, + classes: Set = setOf() +) : Widget(classes + "form-control-range"), FormInput { + + /** + * Range input value. + */ + var value by refreshOnUpdate(value ?: (min as Number?)) { refreshState() } + /** + * The value attribute of the generated HTML input element. + * + * This value is placed directly in generated HTML code, while the [value] property is dynamically + * bound to the range input value. + */ + var startValue by refreshOnUpdate(value) { this.value = it; refresh() } + /** + * Minimal value. + */ + var min by refreshOnUpdate(min) + /** + * Maximal value. + */ + var max by refreshOnUpdate(max) + /** + * Step value. + */ + var step by refreshOnUpdate(step) + /** + * 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 range input is automatically focused. + */ + var autofocus: Boolean? by refreshOnUpdate() + /** + * Determines if the range input is read-only. + */ + var readonly: Boolean? by refreshOnUpdate() + /** + * The size of the input. + */ + override var size: InputSize? by refreshOnUpdate() + /** + * The validation status of the input. + */ + override var validationStatus: ValidationStatus? by refreshOnUpdate() + + init { + this.setInternalEventListener { + change = { + self.changeValue() + } + } + } + + override fun render(): VNode { + return render("input") + } + + override fun getSnClass(): List { + val cl = super.getSnClass().toMutableList() + validationStatus?.let { + cl.add(it.className to true) + } + size?.let { + cl.add(it.className to true) + } + return cl + } + + override fun getSnAttrs(): List { + val sn = super.getSnAttrs().toMutableList() + sn.add("type" to "range") + startValue?.let { + sn.add("value" to it.toString()) + } + name?.let { + sn.add("name" to it) + } + sn.add("min" to "$min") + sn.add("max" to "$max") + sn.add("step" to "$step") + autofocus?.let { + if (it) { + sn.add("autofocus" to "autofocus") + } + } + readonly?.let { + if (it) { + sn.add("readonly" to "readonly") + } + } + if (disabled) { + sn.add("disabled" to "disabled") + } + return sn + } + + override fun afterInsert(node: VNode) { + refreshState() + } + + /** + * Returns the value of the spinner as a String. + * @return value as a String + */ + fun getValueAsString(): String? { + return value?.toString() + } + + /** + * Change value in plus. + */ + open fun stepUp(): RangeInput { + (getElement() as? HTMLInputElement)?.stepUp() + return this + } + + /** + * Change value in minus. + */ + open fun stepDown(): RangeInput { + (getElement() as? HTMLInputElement)?.stepDown() + return this + } + + /** + * @suppress + * Internal function + */ + protected open fun refreshState() { + value?.let { + getElementJQuery()?.`val`(it) + } ?: getElementJQueryD()?.`val`(min) + } + + /** + * @suppress + * Internal function + */ + protected open fun changeValue() { + val v = getElementJQuery()?.`val`() as String? + if (v != null && v.isNotEmpty()) { + this.value = v.toDoubleOrNull() + } else { + this.value = null + } + } + + /** + * Makes the input element focused. + */ + override fun focus() { + getElementJQuery()?.focus() + } + + /** + * Makes the input element blur. + */ + override fun blur() { + getElementJQuery()?.blur() + } +} + +/** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ +fun Container.rangeInput( + value: Number? = null, min: Number = 0, max: Number = 100, step: Number = DEFAULT_STEP, + classes: Set = setOf(), init: (RangeInput.() -> Unit)? = null +): RangeInput { + val rangeInput = RangeInput(value, min, max, step, classes).apply { init?.invoke(this) } + this.add(rangeInput) + return rangeInput +} diff --git a/src/test/kotlin/test/pl/treksoft/kvision/form/range/RangeInputSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/form/range/RangeInputSpec.kt new file mode 100644 index 00000000..70f6d90a --- /dev/null +++ b/src/test/kotlin/test/pl/treksoft/kvision/form/range/RangeInputSpec.kt @@ -0,0 +1,51 @@ +/* + * 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 test.pl.treksoft.kvision.form.select + +import pl.treksoft.kvision.form.range.RangeInput +import pl.treksoft.kvision.panel.Root +import test.pl.treksoft.kvision.DomSpec +import kotlin.browser.document +import kotlin.test.Test + +class RangeInputSpec : DomSpec { + + @Test + fun render() { + run { + val root = Root("test", fixed = true) + val ri = RangeInput(12, 10, 20, 2).apply { + name = "name" + id = "idri" + disabled = true + } + root.add(ri) + val element = document.getElementById("test") + assertEqualsHtml( + "", + element?.innerHTML, + "Should render correct range input control" + ) + } + } + +} diff --git a/src/test/kotlin/test/pl/treksoft/kvision/form/range/RangeSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/form/range/RangeSpec.kt new file mode 100644 index 00000000..26560293 --- /dev/null +++ b/src/test/kotlin/test/pl/treksoft/kvision/form/range/RangeSpec.kt @@ -0,0 +1,51 @@ +/* + * 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 test.pl.treksoft.kvision.form.select + +import pl.treksoft.kvision.form.range.Range +import pl.treksoft.kvision.panel.Root +import test.pl.treksoft.kvision.DomSpec +import kotlin.browser.document +import kotlin.test.Test + +class RangeSpec : DomSpec { + + @Test + fun render() { + run { + val root = Root("test", fixed = true) + val range = Range(12, "name", 10, 20, 2, "Label").apply { + id = "idri" + disabled = true + } + root.add(range) + val element = document.getElementById("test") + val id = range.input.id + assertEqualsHtml( + "
", + element?.innerHTML, + "Should render correct range form control" + ) + } + } + +} -- cgit