diff options
3 files changed, 377 insertions, 7 deletions
diff --git a/kvision-modules/kvision-select-remote/src/main/kotlin/pl/treksoft/kvision/form/select/RemoteSelect.kt b/kvision-modules/kvision-select-remote/src/main/kotlin/pl/treksoft/kvision/form/select/RemoteSelect.kt new file mode 100644 index 00000000..923047e8 --- /dev/null +++ b/kvision-modules/kvision-select-remote/src/main/kotlin/pl/treksoft/kvision/form/select/RemoteSelect.kt @@ -0,0 +1,277 @@ +/* + * 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.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.remote.KVServiceManager +import pl.treksoft.kvision.remote.RemoteSelectOption +import pl.treksoft.kvision.utils.SnOn + +/** + * The form field component for RemoteSelect control. + * + * @constructor + * @param value selected value + * @param serviceManager multiplatform service manager + * @param function multiplatform service method returning the list of options + * @param name the name attribute of the generated HTML input element + * @param multiple allows multiple value selection (multiple values are comma delimited) + * @param ajaxOptions additional options for remote data source + * @param classes a set of CSS class names + * @param label label text bound to the input element + * @param rich determines if [label] can contain HTML code + */ +@Suppress("TooManyFunctions") +open class RemoteSelect<T : Any>( + value: String? = null, + serviceManager: KVServiceManager<T>, + function: T.(String?, String?) -> List<RemoteSelectOption>, + name: String? = null, + multiple: Boolean = false, + ajaxOptions: AjaxOptions? = null, + label: String? = null, + rich: Boolean = false +) : SimplePanel(setOf("form-group")), StringFormControl { + /** + * A value of the selected option. + */ + override var value + get() = input.value + set(value) { + input.value = value + } + /** + * Determines if multiple value selection is allowed. + */ + var multiple + get() = input.multiple + set(value) { + input.multiple = value + } + /** + * Maximal number of selected options. + */ + var maxOptions + get() = input.maxOptions + set(value) { + input.maxOptions = value + } + /** + * The placeholder for the select control. + */ + var placeholder + get() = input.placeholder + set(value) { + input.placeholder = value + } + /** + * The style of the select control. + */ + var style + get() = input.style + set(value) { + input.style = value + } + /** + * The width of the select control. + */ + var selectWidth + get() = input.selectWidth + set(value) { + input.selectWidth = value + } + /** + * The width type of the select control. + */ + var selectWidthType + get() = input.selectWidthType + set(value) { + input.selectWidthType = 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_remoteselect_$counter" + final override val input: RemoteSelectInput<T> = RemoteSelectInput( + value, serviceManager, function, multiple, ajaxOptions, + 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() + } + + /** + * Opens dropdown with options. + */ + open fun showOptions() { + input.showOptions() + } + + /** + * Hides dropdown with options. + */ + open fun hideOptions() { + input.hideOptions() + } + + /** + * Toggles visibility of dropdown with options. + */ + open fun toggleOptions() { + input.toggleOptions() + } + + 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 <T : Any> Container.remoteSelect( + value: String? = null, + serviceManager: KVServiceManager<T>, + function: T.(String?, String?) -> List<RemoteSelectOption>, name: String? = null, + multiple: Boolean = false, ajaxOptions: AjaxOptions? = null, label: String? = null, + rich: Boolean = false, init: (RemoteSelect<T>.() -> Unit)? = null + ): RemoteSelect<T> { + val remoteSelect = + RemoteSelect( + value, + serviceManager, + function, + name, + multiple, + ajaxOptions, + label, + rich + ).apply { init?.invoke(this) } + this.add(remoteSelect) + return remoteSelect + } + } +} diff --git a/kvision-modules/kvision-select-remote/src/main/kotlin/pl/treksoft/kvision/form/select/RemoteSelectInput.kt b/kvision-modules/kvision-select-remote/src/main/kotlin/pl/treksoft/kvision/form/select/RemoteSelectInput.kt index 5986d1fe..1a0fb3fb 100644 --- a/kvision-modules/kvision-select-remote/src/main/kotlin/pl/treksoft/kvision/form/select/RemoteSelectInput.kt +++ b/kvision-modules/kvision-select-remote/src/main/kotlin/pl/treksoft/kvision/form/select/RemoteSelectInput.kt @@ -21,24 +21,42 @@ */ package pl.treksoft.kvision.form.select +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.asDeferred +import kotlinx.coroutines.launch import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.list import kotlinx.serialization.stringify +import pl.treksoft.kvision.core.Container +import pl.treksoft.kvision.remote.CallAgent +import pl.treksoft.kvision.remote.HttpMethod import pl.treksoft.kvision.remote.JsonRpcRequest +import pl.treksoft.kvision.remote.KVServiceManager import pl.treksoft.kvision.remote.RemoteSelectOption -import pl.treksoft.kvision.remote.SpringServiceManager import pl.treksoft.kvision.utils.JSON import pl.treksoft.kvision.utils.obj import kotlin.js.JSON as NativeJSON external fun decodeURIComponent(encodedURI: String): String +/** + * The Select control connected to the multiplatform service. + * + * @constructor + * @param value selected value + * @param serviceManager multiplatform service manager + * @param function multiplatform service method returning the list of options + * @param multiple allows multiple value selection (multiple values are comma delimited) + * @param ajaxOptions additional options for remote data source + * @param classes a set of CSS class names + */ @UseExperimental(ImplicitReflectionSerializer::class) open class RemoteSelectInput<T : Any>( value: String? = null, - serviceManager: SpringServiceManager<T>, - function: T.(String) -> List<RemoteSelectOption>, + serviceManager: KVServiceManager<T>, + function: T.(String?, String?) -> List<RemoteSelectOption>, multiple: Boolean = false, + ajaxOptions: AjaxOptions? = null, classes: Set<String> = setOf() ) : SelectInput(null, value, multiple, null, classes) { init { @@ -48,13 +66,76 @@ open class RemoteSelectInput<T : Any>( val data = obj { q = "{{{q}}}" } - ajaxOptions = AjaxOptions(url, preprocessData = { - JSON.plain.parse(RemoteSelectOption.serializer().list, it.result as String) + val tempAjaxOptions = ajaxOptions ?: AjaxOptions() + this.ajaxOptions = tempAjaxOptions.copy(url = url, preprocessData = { + @Suppress("UnsafeCastFromDynamic") + JSON.plain.parse(RemoteSelectOption.serializer().list, it.result as String).map { + obj { + this.value = it.value + if (it.text != null) this.text = it.text + if (it.className != null) this.`class` = it.className + if (it.disabled) this.disabled = true + if (it.divider) this.divider = true + this.data = obj { + if (it.subtext != null) this.subtext = it.subtext + if (it.icon != null) this.icon = it.icon + if (it.content != null) this.content = it.content + } + } + }.toTypedArray() }, data = data, beforeSend = { _, b -> + @Suppress("UnsafeCastFromDynamic") val q = decodeURIComponent(b.data.substring(2)) - b.data = JSON.plain.stringify(JsonRpcRequest(0, url, listOf(q))) + b.data = JSON.plain.stringify(JsonRpcRequest(0, url, listOf(q, this.value))) true - }, httpType = HttpType.valueOf(method.name)) + }, httpType = HttpType.valueOf(method.name), cache = false, preserveSelected = true) + if (value != null) { + GlobalScope.launch { + val callAgent = CallAgent() + val initials = callAgent.remoteCall( + url, + JSON.plain.stringify(JsonRpcRequest(0, url, listOf(null, value))), + HttpMethod.POST + ).asDeferred().await() + JSON.plain.parse(RemoteSelectOption.serializer().list, initials.result as String).map { + add(SelectOption(it.value, it.text, selected = true)) + } + this@RemoteSelectInput.refreshSelectInput() + } + } + if (this.ajaxOptions?.emptyRequest == true) { + this.setInternalEventListener<RemoteSelect<*>> { + shownBsSelect = { + val input = self.getElementJQuery()?.parent()?.find("input") + input?.trigger("keyup", null) + input?.hide(0) + } + } + + } + } + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun <T : Any> Container.remoteSelectInput( + value: String? = null, + serviceManager: KVServiceManager<T>, + function: T.(String?, String?) -> List<RemoteSelectOption>, + multiple: Boolean = false, + ajaxOptions: AjaxOptions? = null, + classes: Set<String> = setOf(), init: (RemoteSelectInput<T>.() -> Unit)? = null + ): RemoteSelectInput<T> { + val remoteSelectInput = + RemoteSelectInput(value, serviceManager, function, multiple, ajaxOptions, classes).apply { + init?.invoke(this) + } + this.add(remoteSelectInput) + return remoteSelectInput + } } } diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt index 9f28c52c..82a3db48 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt @@ -283,6 +283,18 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() { } } } + if (internalHandlers.shownBsSelect != null) { + if (handlers.shownBsSelect == null) { + handlers.shownBsSelect = internalHandlers.shownBsSelect + } else { + val intc = internalHandlers.shownBsSelect + val c = handlers.shownBsSelect + handlers.shownBsSelect = { e -> + intc?.invoke(e) + c?.invoke(e) + } + } + } handlers } else { null |