Change controls API

This commit is contained in:
Alexander Nozik 2023-11-29 09:41:22 +03:00
parent 80284a99ef
commit 469655092e
12 changed files with 200 additions and 156 deletions

View File

@ -54,9 +54,6 @@
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": { "metadata": {
"jupyter": {
"outputs_hidden": false
},
"tags": [] "tags": []
}, },
"outputs": [], "outputs": [],
@ -83,9 +80,6 @@
"language": "kotlin", "language": "kotlin",
"name": "kotlin" "name": "kotlin"
}, },
"ktnbPluginMetadata": {
"isAddProjectLibrariesToClasspath": false
},
"language_info": { "language_info": {
"codemirror_mode": "text/x-kotlin", "codemirror_mode": "text/x-kotlin",
"file_extension": ".kt", "file_extension": ".kt",
@ -94,6 +88,9 @@
"nbconvert_exporter": "", "nbconvert_exporter": "",
"pygments_lexer": "kotlin", "pygments_lexer": "kotlin",
"version": "1.8.20" "version": "1.8.20"
},
"ktnbPluginMetadata": {
"projectLibraries": []
} }
}, },
"nbformat": 4, "nbformat": 4,

View File

@ -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
}

View File

@ -25,10 +25,7 @@
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": { "metadata": {
"collapsed": false, "collapsed": false
"jupyter": {
"outputs_hidden": false
}
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
@ -84,7 +81,7 @@
"version": "1.8.0-dev-3517" "version": "1.8.0-dev-3517"
}, },
"ktnbPluginMetadata": { "ktnbPluginMetadata": {
"isAddProjectLibrariesToClasspath": false "projectLibraries": []
} }
}, },
"nbformat": 4, "nbformat": 4,

View File

@ -75,7 +75,7 @@ fun main() {
server.openInBrowser() server.openInBrowser()
while (readln() != "exit") { while (readlnOrNull() != "exit") {
} }

View File

@ -2,7 +2,7 @@ package space.kscience.visionforge
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -21,7 +21,7 @@ public abstract class VisionControlEvent : VisionEvent, MetaRepr {
} }
public interface ControlVision : Vision { public interface ControlVision : Vision {
public val controlEventFlow: Flow<VisionControlEvent> public val controlEventFlow: SharedFlow<VisionControlEvent>
public fun dispatchControlEvent(event: VisionControlEvent) public fun dispatchControlEvent(event: VisionControlEvent)
@ -32,21 +32,32 @@ public interface ControlVision : Vision {
} }
} }
/**
* @param payload The optional payload associated with the click event.
*/
@Serializable @Serializable
@SerialName("control.click") @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 { public interface ClickControl : ControlVision {
/**
* Create and dispatch a click event
*/
public fun click(builder: MutableMeta.() -> Unit = {}) { public fun click(builder: MutableMeta.() -> Unit = {}) {
dispatchControlEvent(VisionClickEvent(Meta(builder))) dispatchControlEvent(VisionClickEvent(Meta(builder)))
} }
}
public fun onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job { /**
return controlEventFlow.filterIsInstance<VisionClickEvent>().onEach(block).launchIn(scope) * Register listener
} */
public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job =
controlEventFlow.filterIsInstance<VisionClickEvent>().onEach(block).launchIn(scope)
public companion object {
} @Serializable
} @SerialName("control.valueChange")
public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent()

View File

@ -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<String> 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())
}

View File

@ -9,12 +9,15 @@ import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node import space.kscience.dataforge.meta.node
/**
* @param formId an id of the element in rendered DOM, this form is bound to
*/
@Serializable @Serializable
@SerialName("html.form") @SerialName("html.form")
public class VisionOfHtmlForm( public class VisionOfHtmlForm(
public val formId: String, public val formId: String,
) : VisionOfHtmlInput() { ) : VisionOfHtml() {
public var values: Meta? by mutableProperties.node() public var values: Meta? by properties.node()
} }
public fun <R> TagConsumer<R>.bindForm( public fun <R> TagConsumer<R>.bindForm(

View File

@ -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()
}

View File

@ -252,9 +252,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) { override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) {
listOf( listOf(
numberVisionRenderer(this), numberVisionRenderer(),
textVisionRenderer(this), textVisionRenderer(),
formVisionRenderer(this) formVisionRenderer()
).associateByName() ).associateByName()
} else super<AbstractPlugin>.content(target) } else super<AbstractPlugin>.content(target)

View File

@ -3,66 +3,63 @@ package space.kscience.visionforge
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.html.InputType import kotlinx.html.InputType
import kotlinx.html.js.input import kotlinx.html.js.input
import kotlinx.html.js.label
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.get import org.w3c.dom.get
import org.w3c.xhr.FormData import org.w3c.xhr.FormData
import space.kscience.dataforge.context.debug import space.kscience.dataforge.context.debug
import space.kscience.dataforge.context.logger import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.DynamicMeta import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.Meta import space.kscience.visionforge.html.*
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
internal fun textVisionRenderer(
client: JsVisionClient, private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
): ElementVisionRenderer = ElementVisionRenderer<VisionOfTextField> { name, vision, _ -> vision.useProperty(VisionOfHtml::classes) {
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]" classList.value = classes.joinToString(separator = " ")
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)
}
} }
} }
internal fun numberVisionRenderer(
client: JsVisionClient, private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
): ElementVisionRenderer = ElementVisionRenderer<VisionOfNumberField> { name, vision, _ -> subscribeToVision(inputVision)
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]" inputVision.useProperty(VisionOfHtmlInput::disabled) {
vision.label?.let { disabled = it
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)
}
} }
} }
internal fun JsVisionClient.textVisionRenderer(): ElementVisionRenderer =
ElementVisionRenderer<VisionOfTextField> { 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<VisionOfNumberField> { 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 { internal fun FormData.toMeta(): Meta {
@Suppress("UNUSED_VARIABLE") val formData = this @Suppress("UNUSED_VARIABLE") val formData = this
//val res = js("Object.fromEntries(formData);") //val res = js("Object.fromEntries(formData);")
@ -86,28 +83,29 @@ internal fun FormData.toMeta(): Meta {
return DynamicMeta(`object`) return DynamicMeta(`object`)
} }
internal fun formVisionRenderer( internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer =
client: JsVisionClient, ElementVisionRenderer<VisionOfHtmlForm> { visionName, vision, _ ->
): ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlForm> { name, vision, _ ->
val form = document.getElementById(vision.formId) as? HTMLFormElement val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form") ?: 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 -> logger.debug { "Adding hooks to form with id = '$vision.formId'" }
client.logger.debug{"Updating form '${vision.formId}' with values $values"}
val inputs = form.getElementsByTagName("input") vision.useProperty(VisionOfHtmlForm::values) { values ->
values?.valueSequence()?.forEach { (token, value) -> logger.debug { "Updating form '${vision.formId}' with values $values" }
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString() val inputs = form.getElementsByTagName("input")
values?.valueSequence()?.forEach { (token, value) ->
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
}
} }
}
form.onsubmit = { event -> form.onsubmit = { event ->
event.preventDefault() event.preventDefault()
val formData = FormData(form).toMeta() val formData = FormData(form).toMeta()
client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData) notifyPropertyChanged(visionName, VisionOfHtmlForm::values.name, formData)
console.info("Sent: ${formData.toMap()}") console.info("Sent: ${formData.toMap()}")
false false
} }
} }

View File

@ -7,7 +7,6 @@ import org.jetbrains.kotlinx.jupyter.api.declare
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.* import space.kscience.visionforge.html.*
@ -17,7 +16,6 @@ import kotlin.random.nextUInt
/** /**
* A base class for different Jupyter VF integrations * A base class for different Jupyter VF integrations
*/ */
@DFExperimental
public abstract class VisionForgeIntegration( public abstract class VisionForgeIntegration(
public val visionManager: VisionManager, public val visionManager: VisionManager,
) : JupyterIntegration(), ContextAware { ) : JupyterIntegration(), ContextAware {

View File

@ -1,14 +1,14 @@
package space.kscience.visionforge.jupyter 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 org.jetbrains.kotlinx.jupyter.api.libraries.resources
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.gdml.Gdml import space.kscience.gdml.Gdml
import space.kscience.plotly.Plot import space.kscience.plotly.Plot
import space.kscience.plotly.PlotlyPage import space.kscience.plotly.PlotlyPage
import space.kscience.plotly.StaticPlotlyRenderer import space.kscience.plotly.StaticPlotlyRenderer
import space.kscience.tables.* import space.kscience.tables.Table
import space.kscience.visionforge.gdml.toVision import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.html.VisionPage
@ -21,7 +21,6 @@ import space.kscience.visionforge.tables.toVision
import space.kscience.visionforge.visionManager import space.kscience.visionforge.visionManager
@DFExperimental
public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) { public class JupyterCommonIntegration : VisionForgeIntegration(CONTEXT.visionManager) {
override fun Builder.afterLoaded(vf: VisionForge) { override fun Builder.afterLoaded(vf: VisionForge) {