From 469655092e8c7f871dfb610f1c34555d82da0d81 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 29 Nov 2023 09:41:22 +0300 Subject: [PATCH] Change controls API --- demo/playground/notebooks/common-demo.ipynb | 9 +- demo/playground/notebooks/controls.ipynb | 45 ++++++ demo/playground/notebooks/dynamic-demo.ipynb | 7 +- .../src/jvmMain/kotlin/formServer.kt | 2 +- .../kscience/visionforge/ControlVision.kt | 29 ++-- .../kscience/visionforge/html/VisionOfHtml.kt | 54 ++++++++ .../visionforge/html/VisionOfHtmlForm.kt | 7 +- .../visionforge/html/VisionOfHtmlInput.kt | 58 -------- .../kscience/visionforge/JsVisionClient.kt | 6 +- .../kscience/visionforge/inputRenderers.kt | 130 +++++++++--------- .../jvmMain/kotlin/VisionForgeIntegration.kt | 2 - .../kotlin/JupyterCommonIntegration.kt | 7 +- 12 files changed, 200 insertions(+), 156 deletions(-) create mode 100644 demo/playground/notebooks/controls.ipynb create mode 100644 visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt delete mode 100644 visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt diff --git a/demo/playground/notebooks/common-demo.ipynb b/demo/playground/notebooks/common-demo.ipynb index 78797545..caa7306f 100644 --- a/demo/playground/notebooks/common-demo.ipynb +++ b/demo/playground/notebooks/common-demo.ipynb @@ -54,9 +54,6 @@ "cell_type": "code", "execution_count": null, "metadata": { - "jupyter": { - "outputs_hidden": false - }, "tags": [] }, "outputs": [], @@ -83,9 +80,6 @@ "language": "kotlin", "name": "kotlin" }, - "ktnbPluginMetadata": { - "isAddProjectLibrariesToClasspath": false - }, "language_info": { "codemirror_mode": "text/x-kotlin", "file_extension": ".kt", @@ -94,6 +88,9 @@ "nbconvert_exporter": "", "pygments_lexer": "kotlin", "version": "1.8.20" + }, + "ktnbPluginMetadata": { + "projectLibraries": [] } }, "nbformat": 4, diff --git a/demo/playground/notebooks/controls.ipynb b/demo/playground/notebooks/controls.ipynb new file mode 100644 index 00000000..8552a178 --- /dev/null +++ b/demo/playground/notebooks/controls.ipynb @@ -0,0 +1,45 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "USE(JupyterCommonIntegration())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "1.9.0", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "projectDependencies": true + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/demo/playground/notebooks/dynamic-demo.ipynb b/demo/playground/notebooks/dynamic-demo.ipynb index ac70b4c2..3fcd31e3 100644 --- a/demo/playground/notebooks/dynamic-demo.ipynb +++ b/demo/playground/notebooks/dynamic-demo.ipynb @@ -25,10 +25,7 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } + "collapsed": false }, "outputs": [], "source": [ @@ -84,7 +81,7 @@ "version": "1.8.0-dev-3517" }, "ktnbPluginMetadata": { - "isAddProjectLibrariesToClasspath": false + "projectLibraries": [] } }, "nbformat": 4, diff --git a/demo/playground/src/jvmMain/kotlin/formServer.kt b/demo/playground/src/jvmMain/kotlin/formServer.kt index d832d85a..21d2d4a7 100644 --- a/demo/playground/src/jvmMain/kotlin/formServer.kt +++ b/demo/playground/src/jvmMain/kotlin/formServer.kt @@ -75,7 +75,7 @@ fun main() { server.openInBrowser() - while (readln() != "exit") { + while (readlnOrNull() != "exit") { } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt index 38ee4d7f..e0d44930 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt @@ -2,7 +2,7 @@ package space.kscience.visionforge import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -21,7 +21,7 @@ public abstract class VisionControlEvent : VisionEvent, MetaRepr { } public interface ControlVision : Vision { - public val controlEventFlow: Flow + public val controlEventFlow: SharedFlow public fun dispatchControlEvent(event: VisionControlEvent) @@ -32,21 +32,32 @@ public interface ControlVision : Vision { } } +/** + * @param payload The optional payload associated with the click event. + */ @Serializable @SerialName("control.click") -public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() +public class VisionClickEvent(public val payload: Meta = Meta.EMPTY) : VisionControlEvent() { + override val meta: Meta get() = Meta { ::payload.name put payload } +} public interface ClickControl : ControlVision { + /** + * Create and dispatch a click event + */ public fun click(builder: MutableMeta.() -> Unit = {}) { dispatchControlEvent(VisionClickEvent(Meta(builder))) } +} - public fun onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job { - return controlEventFlow.filterIsInstance().onEach(block).launchIn(scope) - } +/** + * Register listener + */ +public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job = + controlEventFlow.filterIsInstance().onEach(block).launchIn(scope) - public companion object { - } -} \ No newline at end of file +@Serializable +@SerialName("control.valueChange") +public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent() \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt new file mode 100644 index 00000000..d9f09ec1 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt @@ -0,0 +1,54 @@ +package space.kscience.visionforge.html + +import kotlinx.html.InputType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.names.asName +import space.kscience.visionforge.AbstractVision + + +@Serializable +public abstract class VisionOfHtml: AbstractVision(){ + public var classes: List by properties.stringList(*emptyArray()) +} + +@Serializable +@SerialName("html.input") +public open class VisionOfHtmlInput( + public val inputType: String, +) : VisionOfHtml() { + public var value : Value? by properties.value() + public var disabled: Boolean by properties.boolean { false } + public var fieldName: String? by properties.string() +} + + +@Serializable +@SerialName("html.text") +public class VisionOfTextField : VisionOfHtmlInput(InputType.text.realValue) { + public var text: String? by properties.string(key = VisionOfHtmlInput::value.name.asName()) +} + +@Serializable +@SerialName("html.checkbox") +public class VisionOfCheckbox : VisionOfHtmlInput(InputType.checkBox.realValue) { + public var checked: Boolean? by properties.boolean(key = VisionOfHtmlInput::value.name.asName()) +} + +@Serializable +@SerialName("html.number") +public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) { + public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) +} + +@Serializable +@SerialName("html.range") +public class VisionOfRangeField( + public val min: Double, + public val max: Double, + public val step: Double = 1.0, +) : VisionOfHtmlInput(InputType.range.realValue) { + public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) +} + diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt index d9c0347d..e56af874 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt @@ -9,12 +9,15 @@ import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.node +/** + * @param formId an id of the element in rendered DOM, this form is bound to + */ @Serializable @SerialName("html.form") public class VisionOfHtmlForm( public val formId: String, -) : VisionOfHtmlInput() { - public var values: Meta? by mutableProperties.node() +) : VisionOfHtml() { + public var values: Meta? by properties.node() } public fun TagConsumer.bindForm( diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt deleted file mode 100644 index d2bb2c52..00000000 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlInput.kt +++ /dev/null @@ -1,58 +0,0 @@ -package space.kscience.visionforge.html - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import space.kscience.dataforge.meta.boolean -import space.kscience.dataforge.meta.number -import space.kscience.dataforge.meta.string -import space.kscience.dataforge.names.Name -import space.kscience.visionforge.AbstractVision -import space.kscience.visionforge.Vision - -//TODO replace by something -internal val Vision.mutableProperties get() = properties.getMeta(Name.EMPTY, false, false) - -@Serializable -public abstract class VisionOfHtmlInput : AbstractVision() { - public var disabled: Boolean by mutableProperties.boolean { false } -} - -@Serializable -@SerialName("html.text") -public class VisionOfTextField( - public val label: String? = null, - public val name: String? = null, -) : VisionOfHtmlInput() { - public var text: String? by mutableProperties.string() -} - -@Serializable -@SerialName("html.checkbox") -public class VisionOfCheckbox( - public val label: String? = null, - public val name: String? = null, -) : VisionOfHtmlInput() { - public var checked: Boolean? by mutableProperties.boolean() -} - -@Serializable -@SerialName("html.number") -public class VisionOfNumberField( - public val label: String? = null, - public val name: String? = null, -) : VisionOfHtmlInput() { - public var value: Number? by mutableProperties.number() -} - -@Serializable -@SerialName("html.range") -public class VisionOfRangeField( - public val min: Double, - public val max: Double, - public val step: Double = 1.0, - public val label: String? = null, - public val name: String? = null, -) : VisionOfHtmlInput() { - public var value: Number? by mutableProperties.number() -} - diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt index b9e0ef93..d164e5ea 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -252,9 +252,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { override fun content(target: String): Map = if (target == ElementVisionRenderer.TYPE) { listOf( - numberVisionRenderer(this), - textVisionRenderer(this), - formVisionRenderer(this) + numberVisionRenderer(), + textVisionRenderer(), + formVisionRenderer() ).associateByName() } else super.content(target) diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt index ff84c403..8b07e177 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt @@ -3,66 +3,63 @@ package space.kscience.visionforge import kotlinx.browser.document import kotlinx.html.InputType import kotlinx.html.js.input -import kotlinx.html.js.label import kotlinx.html.js.onChangeFunction +import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLInputElement import org.w3c.dom.get import org.w3c.xhr.FormData import space.kscience.dataforge.context.debug import space.kscience.dataforge.context.logger -import space.kscience.dataforge.meta.DynamicMeta -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.toMap -import space.kscience.dataforge.meta.valueSequence -import space.kscience.visionforge.html.VisionOfHtmlForm -import space.kscience.visionforge.html.VisionOfNumberField -import space.kscience.visionforge.html.VisionOfTextField +import space.kscience.dataforge.meta.* +import space.kscience.visionforge.html.* -internal fun textVisionRenderer( - client: JsVisionClient, -): ElementVisionRenderer = ElementVisionRenderer { name, vision, _ -> - val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]" - vision.label?.let { - label { - htmlFor = fieldName - +it - } - } - input { - type = InputType.text - this.name = fieldName - vision.useProperty(VisionOfTextField::text) { - value = it ?: "" - } - onChangeFunction = { - client.notifyPropertyChanged(name, VisionOfTextField::text.name, value) - } + +private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { + vision.useProperty(VisionOfHtml::classes) { + classList.value = classes.joinToString(separator = " ") } } -internal fun numberVisionRenderer( - client: JsVisionClient, -): ElementVisionRenderer = ElementVisionRenderer { name, vision, _ -> - val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]" - vision.label?.let { - label { - htmlFor = fieldName - +it - } - } - input { - type = InputType.text - this.name = fieldName - vision.useProperty(VisionOfNumberField::value) { - value = it?.toDouble() ?: 0.0 - } - onChangeFunction = { - client.notifyPropertyChanged(name, VisionOfNumberField::value.name, value) - } + +private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) { + subscribeToVision(inputVision) + inputVision.useProperty(VisionOfHtmlInput::disabled) { + disabled = it } } + +internal fun JsVisionClient.textVisionRenderer(): ElementVisionRenderer = + ElementVisionRenderer { visionName, vision, _ -> + input { + type = InputType.text + onChangeFunction = { + notifyPropertyChanged(visionName, VisionOfTextField::text.name, value) + } + }.apply { + subscribeToInput(vision) + vision.useProperty(VisionOfTextField::text) { + value = (it ?: "").asValue() + } + } + } + +internal fun JsVisionClient.numberVisionRenderer(): ElementVisionRenderer = + ElementVisionRenderer { visionName, vision, _ -> + input { + type = InputType.text + onChangeFunction = { + notifyPropertyChanged(visionName, VisionOfNumberField::value.name, value) + } + }.apply { + subscribeToInput(vision) + vision.useProperty(VisionOfNumberField::value) { + value = (it?.double ?: 0.0).asValue() + } + } + } + internal fun FormData.toMeta(): Meta { @Suppress("UNUSED_VARIABLE") val formData = this //val res = js("Object.fromEntries(formData);") @@ -86,28 +83,29 @@ internal fun FormData.toMeta(): Meta { return DynamicMeta(`object`) } -internal fun formVisionRenderer( - client: JsVisionClient, -): ElementVisionRenderer = ElementVisionRenderer { name, vision, _ -> +internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer = + ElementVisionRenderer { visionName, vision, _ -> - val form = document.getElementById(vision.formId) as? HTMLFormElement - ?: error("An element with id = '${vision.formId} is not a form") + val form = document.getElementById(vision.formId) as? HTMLFormElement + ?: error("An element with id = '${vision.formId} is not a form") - client.logger.debug{"Adding hooks to form with id = '$vision.formId'"} + form.subscribeToVision(vision) - vision.useProperty(VisionOfHtmlForm::values) { values -> - client.logger.debug{"Updating form '${vision.formId}' with values $values"} - val inputs = form.getElementsByTagName("input") - values?.valueSequence()?.forEach { (token, value) -> - (inputs[token.toString()] as? HTMLInputElement)?.value = value.toString() + logger.debug { "Adding hooks to form with id = '$vision.formId'" } + + vision.useProperty(VisionOfHtmlForm::values) { values -> + logger.debug { "Updating form '${vision.formId}' with values $values" } + val inputs = form.getElementsByTagName("input") + values?.valueSequence()?.forEach { (token, value) -> + (inputs[token.toString()] as? HTMLInputElement)?.value = value.toString() + } } - } - form.onsubmit = { event -> - event.preventDefault() - val formData = FormData(form).toMeta() - client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData) - console.info("Sent: ${formData.toMap()}") - false - } -} \ No newline at end of file + form.onsubmit = { event -> + event.preventDefault() + val formData = FormData(form).toMeta() + notifyPropertyChanged(visionName, VisionOfHtmlForm::values.name, formData) + console.info("Sent: ${formData.toMap()}") + false + } + } \ No newline at end of file diff --git a/visionforge-jupyter/src/jvmMain/kotlin/VisionForgeIntegration.kt b/visionforge-jupyter/src/jvmMain/kotlin/VisionForgeIntegration.kt index afc2ecc2..935f183c 100644 --- a/visionforge-jupyter/src/jvmMain/kotlin/VisionForgeIntegration.kt +++ b/visionforge-jupyter/src/jvmMain/kotlin/VisionForgeIntegration.kt @@ -7,7 +7,6 @@ import org.jetbrains.kotlinx.jupyter.api.declare import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.ContextAware -import space.kscience.dataforge.misc.DFExperimental import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionManager import space.kscience.visionforge.html.* @@ -17,7 +16,6 @@ import kotlin.random.nextUInt /** * A base class for different Jupyter VF integrations */ -@DFExperimental public abstract class VisionForgeIntegration( public val visionManager: VisionManager, ) : JupyterIntegration(), ContextAware { diff --git a/visionforge-jupyter/visionforge-jupyter-common/src/jvmMain/kotlin/JupyterCommonIntegration.kt b/visionforge-jupyter/visionforge-jupyter-common/src/jvmMain/kotlin/JupyterCommonIntegration.kt index 6200bd5d..2f7988f8 100644 --- a/visionforge-jupyter/visionforge-jupyter-common/src/jvmMain/kotlin/JupyterCommonIntegration.kt +++ b/visionforge-jupyter/visionforge-jupyter-common/src/jvmMain/kotlin/JupyterCommonIntegration.kt @@ -1,14 +1,14 @@ package space.kscience.visionforge.jupyter -import kotlinx.html.* +import kotlinx.html.div +import kotlinx.html.p import org.jetbrains.kotlinx.jupyter.api.libraries.resources import space.kscience.dataforge.context.Context -import space.kscience.dataforge.misc.DFExperimental import space.kscience.gdml.Gdml import space.kscience.plotly.Plot import space.kscience.plotly.PlotlyPage import space.kscience.plotly.StaticPlotlyRenderer -import space.kscience.tables.* +import space.kscience.tables.Table import space.kscience.visionforge.gdml.toVision import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.VisionPage @@ -21,7 +21,6 @@ import space.kscience.visionforge.tables.toVision import space.kscience.visionforge.visionManager -@DFExperimental public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) { override fun Builder.afterLoaded(vf: VisionForge) {