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",
"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,

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",
"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,

View File

@ -75,7 +75,7 @@ fun main() {
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.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<VisionControlEvent>
public val controlEventFlow: SharedFlow<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
@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<VisionClickEvent>().onEach(block).launchIn(scope)
}
public companion object {
/**
* Register listener
*/
public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job =
controlEventFlow.filterIsInstance<VisionClickEvent>().onEach(block).launchIn(scope)
}
}
@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.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 <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) {
listOf(
numberVisionRenderer(this),
textVisionRenderer(this),
formVisionRenderer(this)
numberVisionRenderer(),
textVisionRenderer(),
formVisionRenderer()
).associateByName()
} else super<AbstractPlugin>.content(target)

View File

@ -3,62 +3,59 @@ 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<VisionOfTextField> { name, vision, _ ->
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
vision.label?.let {
label {
htmlFor = fieldName
+it
private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
vision.useProperty(VisionOfHtml::classes) {
classList.value = classes.joinToString(separator = " ")
}
}
private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
subscribeToVision(inputVision)
inputVision.useProperty(VisionOfHtmlInput::disabled) {
disabled = it
}
}
internal fun JsVisionClient.textVisionRenderer(): ElementVisionRenderer =
ElementVisionRenderer<VisionOfTextField> { visionName, vision, _ ->
input {
type = InputType.text
this.name = fieldName
onChangeFunction = {
notifyPropertyChanged(visionName, VisionOfTextField::text.name, value)
}
}.apply {
subscribeToInput(vision)
vision.useProperty(VisionOfTextField::text) {
value = it ?: ""
}
onChangeFunction = {
client.notifyPropertyChanged(name, VisionOfTextField::text.name, value)
value = (it ?: "").asValue()
}
}
}
internal fun numberVisionRenderer(
client: JsVisionClient,
): ElementVisionRenderer = ElementVisionRenderer<VisionOfNumberField> { name, vision, _ ->
val fieldName = vision.name ?: "input[${vision.hashCode().toUInt()}]"
vision.label?.let {
label {
htmlFor = fieldName
+it
}
}
internal fun JsVisionClient.numberVisionRenderer(): ElementVisionRenderer =
ElementVisionRenderer<VisionOfNumberField> { visionName, vision, _ ->
input {
type = InputType.text
this.name = fieldName
vision.useProperty(VisionOfNumberField::value) {
value = it?.toDouble() ?: 0.0
}
onChangeFunction = {
client.notifyPropertyChanged(name, VisionOfNumberField::value.name, value)
notifyPropertyChanged(visionName, VisionOfNumberField::value.name, value)
}
}.apply {
subscribeToInput(vision)
vision.useProperty(VisionOfNumberField::value) {
value = (it?.double ?: 0.0).asValue()
}
}
}
@ -86,17 +83,18 @@ internal fun FormData.toMeta(): Meta {
return DynamicMeta(`object`)
}
internal fun formVisionRenderer(
client: JsVisionClient,
): ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlForm> { name, vision, _ ->
internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlForm> { visionName, vision, _ ->
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)
logger.debug { "Adding hooks to form with id = '$vision.formId'" }
vision.useProperty(VisionOfHtmlForm::values) { values ->
client.logger.debug{"Updating form '${vision.formId}' with 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()
@ -106,7 +104,7 @@ internal fun formVisionRenderer(
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
client.notifyPropertyChanged(name, VisionOfHtmlForm::values.name, formData)
notifyPropertyChanged(visionName, VisionOfHtmlForm::values.name, formData)
console.info("Sent: ${formData.toMap()}")
false
}

View File

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

View File

@ -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) {