Compare commits

...

3 Commits

Author SHA1 Message Date
f40bed7bb9 Implement and test input elements 2023-12-10 17:06:58 +03:00
05b87857f4 Add input listeners 2023-12-06 23:01:22 +03:00
bce61c0fb0 Html input events 2023-12-05 18:50:26 +03:00
17 changed files with 339 additions and 139 deletions

View File

@ -12,7 +12,7 @@ val fxVersion by extra("11")
allprojects {
group = "space.kscience"
version = "0.3.0-dev-17"
version = "0.3.0-dev-18"
}
subprojects {

View File

@ -0,0 +1,59 @@
package space.kscience.visionforge.examples
import kotlinx.html.p
import space.kscience.visionforge.VisionControlEvent
import space.kscience.visionforge.html.*
import space.kscience.visionforge.onClick
fun main() = serve {
val events = ArrayDeque<VisionControlEvent>(10)
val html = VisionOfPlainHtml()
fun pushEvent(event: VisionControlEvent) {
events.addFirst(event)
if (events.size >= 10) {
events.removeLast()
}
html.content {
events.forEach { event ->
p {
text(event.toString())
}
}
}
}
vision {
htmlCheckBox {
checked = true
onValueChange(context) {
pushEvent(this)
}
}
}
vision {
htmlRangeField(1, 10) {
numberValue = 4
onValueChange(context) {
pushEvent(this)
}
}
}
vision {
button("Click me"){
onClick(context){
pushEvent(this)
}
}
}
vision(html)
}

View File

@ -13,6 +13,7 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionClient
import space.kscience.visionforge.react.render
import space.kscience.visionforge.solid.Solid
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
@ -26,9 +27,9 @@ public class ThreeWithControlsPlugin : AbstractPlugin(), ElementVisionRenderer {
override fun rateVision(vision: Vision): Int =
if (vision is Solid) ElementVisionRenderer.DEFAULT_RATING * 2 else ElementVisionRenderer.ZERO_RATING
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
if(meta["controls.enabled"].boolean == false){
three.render(element, name, vision, meta)
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
if (meta["controls.enabled"].boolean == false) {
three.render(element, client, name, vision, meta)
} else {
space.kscience.visionforge.react.createRoot(element).render {
child(ThreeCanvasWithControls) {

View File

@ -8,13 +8,13 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaRepr
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
@Serializable
@SerialName("control")
public abstract class VisionControlEvent : VisionEvent, MetaRepr {
public sealed class VisionControlEvent : VisionEvent, MetaRepr {
public abstract val meta: Meta
override fun toMeta(): Meta = meta
@ -23,30 +23,41 @@ public abstract class VisionControlEvent : VisionEvent, MetaRepr {
public interface ControlVision : Vision {
public val controlEventFlow: SharedFlow<VisionControlEvent>
public fun dispatchControlEvent(event: VisionControlEvent)
public suspend fun dispatchControlEvent(event: VisionControlEvent)
override fun receiveEvent(event: VisionEvent) {
override suspend fun receiveEvent(event: VisionEvent) {
if (event is VisionControlEvent) {
dispatchControlEvent(event)
} else super.receiveEvent(event)
}
}
/**
* @param payload The optional payload associated with the click event.
*/
@Serializable
@SerialName("control.click")
public class VisionClickEvent(public val payload: Meta = Meta.EMPTY) : VisionControlEvent() {
override val meta: Meta get() = Meta { ::payload.name put payload }
public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() {
public val payload: Meta? by meta.node()
public val name: Name? get() = meta["name"].string?.parseAsName()
override fun toString(): String = meta.toString()
}
public fun VisionClickEvent(payload: Meta = Meta.EMPTY, name: Name? = null): VisionClickEvent = VisionClickEvent(
Meta {
VisionClickEvent::payload.name put payload
VisionClickEvent::name.name put name.toString()
}
)
public interface ClickControl : ControlVision {
/**
* Create and dispatch a click event
*/
public fun click(builder: MutableMeta.() -> Unit = {}) {
public suspend fun click(builder: MutableMeta.() -> Unit = {}) {
dispatchControlEvent(VisionClickEvent(Meta(builder)))
}
}
@ -60,4 +71,21 @@ public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClic
@Serializable
@SerialName("control.valueChange")
public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent()
public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent() {
public val value: Value? get() = meta.value
/**
* The name of a control that fired the event
*/
public val name: Name? get() = meta["name"]?.string?.parseAsName()
override fun toString(): String = meta.toString()
}
public fun VisionValueChangeEvent(value: Value?, name: Name? = null): VisionValueChangeEvent = VisionValueChangeEvent(
Meta {
this.value = value
name?.let { set("name", it.toString()) }
}
)

View File

@ -50,7 +50,7 @@ public interface Vision : Described {
/**
* Receive and process a generic [VisionEvent].
*/
public fun receiveEvent(event: VisionEvent) {
public suspend fun receiveEvent(event: VisionEvent) {
if(event is VisionChange) update(event)
else manager?.logger?.warn { "Undispatched event: $event" }
}

View File

@ -1,9 +1,7 @@
package space.kscience.visionforge
import kotlinx.coroutines.launch
import space.kscience.dataforge.context.Plugin
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaRepr
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
@ -32,10 +30,4 @@ public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: St
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Boolean) {
notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
}
public fun VisionClient.sendEvent(targetName: Name, payload: MetaRepr): Unit {
context.launch {
sendEvent(targetName, VisionMetaEvent(payload.toMeta()))
}
}

View File

@ -69,12 +69,14 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont
defaultDeserializer { SimpleVisionGroup.serializer() }
subclass(NullVision.serializer())
subclass(SimpleVisionGroup.serializer())
subclass(VisionOfPlainHtml.serializer())
subclass(VisionOfHtmlInput.serializer())
subclass(VisionOfNumberField.serializer())
subclass(VisionOfTextField.serializer())
subclass(VisionOfCheckbox.serializer())
subclass(VisionOfRangeField.serializer())
subclass(VisionOfHtmlForm.serializer())
subclass(VisionOfHtmlButton.serializer())
}
}

View File

@ -1,13 +1,21 @@
package space.kscience.visionforge.html
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.html.DIV
import kotlinx.html.InputType
import kotlinx.html.TagConsumer
import kotlinx.html.div
import kotlinx.html.stream.createHTML
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.AbstractVision
import space.kscience.visionforge.ControlVision
import space.kscience.visionforge.VisionControlEvent
import space.kscience.visionforge.VisionValueChangeEvent
@Serializable
@ -21,8 +29,10 @@ public class VisionOfPlainHtml : VisionOfHtml() {
public var content: String? by properties.string()
}
public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) {
content = createHTML().apply(block).finalize()
public fun VisionOfPlainHtml.content(block: DIV.() -> Unit) {
content = createHTML().apply {
div(block = block)
}.finalize()
}
@Suppress("UnusedReceiverParameter")
@ -48,17 +58,40 @@ public enum class InputFeedbackMode {
NONE
}
public abstract class VisionOfHtmlControl: VisionOfHtml(), ControlVision{
@Transient
private val mutableControlEventFlow = MutableSharedFlow<VisionControlEvent>()
override val controlEventFlow: SharedFlow<VisionControlEvent>
get() = mutableControlEventFlow
override suspend fun dispatchControlEvent(event: VisionControlEvent) {
mutableControlEventFlow.emit(event)
}
}
@Serializable
@SerialName("html.input")
public open class VisionOfHtmlInput(
public val inputType: String,
public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE,
) : VisionOfHtml() {
) : VisionOfHtmlControl() {
public var value: Value? by properties.value()
public var disabled: Boolean by properties.boolean { false }
public var fieldName: String? by properties.string()
}
/**
* Trigger [callback] on each value change
*/
public fun VisionOfHtmlInput.onValueChange(
scope: CoroutineScope = manager?.context ?: error("Coroutine context is not resolved for $this"),
callback: suspend VisionValueChangeEvent.() -> Unit,
): Job = controlEventFlow.filterIsInstance<VisionValueChangeEvent>().onEach(callback).launchIn(scope)
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlInput(
inputType: String,
@ -91,7 +124,7 @@ public inline fun VisionOutput.htmlCheckBox(
@Serializable
@SerialName("html.number")
public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) {
public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
public var numberValue: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
}
@Suppress("UnusedReceiverParameter")
@ -106,14 +139,14 @@ public class VisionOfRangeField(
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())
public var numberValue: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName())
}
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlRangeField(
min: Double,
max: Double,
step: Double = 1.0,
min: Number,
max: Number,
step: Number = 1.0,
block: VisionOfRangeField.() -> Unit = {},
): VisionOfRangeField = VisionOfRangeField(min, max, step).apply(block)
): VisionOfRangeField = VisionOfRangeField(min.toDouble(), max.toDouble(), step.toDouble()).apply(block)

View File

@ -8,6 +8,8 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node
import space.kscience.dataforge.meta.string
import space.kscience.visionforge.ClickControl
/**
* @param formId an id of the element in rendered DOM, this form is bound to
@ -16,7 +18,7 @@ import space.kscience.dataforge.meta.node
@SerialName("html.form")
public class VisionOfHtmlForm(
public val formId: String,
) : VisionOfHtml() {
) : VisionOfHtmlControl() {
public var values: Meta? by properties.node()
}
@ -26,4 +28,21 @@ public fun <R> TagConsumer<R>.bindForm(
): R = form {
this.id = visionOfForm.formId
builder()
}
@Serializable
@SerialName("html.button")
public class VisionOfHtmlButton : VisionOfHtmlControl(), ClickControl {
public var label: String? by properties.string()
}
@Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.button(
text: String,
block: VisionOfHtmlButton.() -> Unit = {},
): VisionOfHtmlButton = VisionOfHtmlButton().apply {
label = text
block()
}

View File

@ -34,7 +34,13 @@ public interface ElementVisionRenderer : Named {
* Display the [vision] inside a given [element] replacing its current content.
* @param meta additional parameters for rendering container
*/
public fun render(element: Element, name: Name, vision: Vision, meta: Meta = Meta.EMPTY)
public fun render(
element: Element,
client: VisionClient,
name: Name,
vision: Vision,
meta: Meta = Meta.EMPTY,
)
public companion object {
public const val TYPE: String = "elementVisionRenderer"
@ -49,7 +55,7 @@ public interface ElementVisionRenderer : Named {
public class SingleTypeVisionRenderer<T : Vision>(
public val kClass: KClass<T>,
private val acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING,
private val renderFunction: TagConsumer<HTMLElement>.(name: Name, vision: T, meta: Meta) -> Unit,
private val renderFunction: TagConsumer<HTMLElement>.(name: Name, client: VisionClient, vision: T, meta: Meta) -> Unit,
) : ElementVisionRenderer {
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
@ -60,15 +66,21 @@ public class SingleTypeVisionRenderer<T : Vision>(
override fun rateVision(vision: Vision): Int =
if (vision::class == kClass) acceptRating else ElementVisionRenderer.ZERO_RATING
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
override fun render(
element: Element,
client: VisionClient,
name: Name,
vision: Vision,
meta: Meta,
) {
element.clear()
element.append {
renderFunction(name, kClass.cast(vision), meta)
renderFunction(name, client, kClass.cast(vision), meta)
}
}
}
public inline fun <reified T : Vision> ElementVisionRenderer(
acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING,
noinline renderFunction: TagConsumer<HTMLElement>.(name: Name, vision: T, meta: Meta) -> Unit,
noinline renderFunction: TagConsumer<HTMLElement>.(name: Name, client: VisionClient, vision: T, meta: Meta) -> Unit,
): ElementVisionRenderer = SingleTypeVisionRenderer(T::class, acceptRating, renderFunction)

View File

@ -94,8 +94,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
private fun renderVision(element: Element, name: Name, vision: Vision, outputMeta: Meta) {
vision.setAsRoot(visionManager)
val renderer = findRendererFor(vision) ?: error("Could not find renderer for ${vision::class}")
renderer.render(element, name, vision, outputMeta)
val renderer: ElementVisionRenderer =
findRendererFor(vision) ?: error("Could not find renderer for ${vision::class}")
renderer.render(element, this, name, vision, outputMeta)
}
private fun startVisionUpdate(element: Element, visionName: Name, vision: Vision, outputMeta: Meta) {
@ -134,7 +135,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
}
logger.debug { "Got $event for output with name $visionName" }
vision.receiveEvent(event)
context.launch {
vision.receiveEvent(event)
}
} else {
logger.error { "WebSocket message data is not a string" }
}
@ -261,7 +264,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
numberVisionRenderer,
textVisionRenderer,
rangeVisionRenderer,
formVisionRenderer
formVisionRenderer,
buttonVisionRenderer
).associateByName()
} else super<AbstractPlugin>.content(target)

View File

@ -0,0 +1,90 @@
package space.kscience.visionforge
import kotlinx.browser.document
import kotlinx.coroutines.launch
import kotlinx.html.ButtonType
import kotlinx.html.js.button
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.*
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.VisionOfHtmlButton
import space.kscience.visionforge.html.VisionOfHtmlForm
internal fun FormData.toMeta(): Meta {
@Suppress("UNUSED_VARIABLE") val formData = this
//val res = js("Object.fromEntries(formData);")
val `object` = js("{}")
//language=JavaScript
js(
"""
formData.forEach(function(value, key){
// Reflect.has in favor of: object.hasOwnProperty(key)
if(!Reflect.has(object, key)){
object[key] = value;
return;
}
if(!Array.isArray(object[key])){
object[key] = [object[key]];
}
object[key].push(value);
});
"""
)
return DynamicMeta(`object`)
}
public fun VisionClient.sendMetaEvent(targetName: Name, payload: MetaRepr): Unit {
context.launch {
sendEvent(targetName, VisionMetaEvent(payload.toMeta()))
}
}
internal val formVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlForm> { name, client, vision, _ ->
val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form")
form.subscribeToVision(vision)
vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" }
vision.useProperty(VisionOfHtmlForm::values) { values ->
vision.manager?.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.sendMetaEvent(name, formData)
console.info("Sent: ${formData.toMap()}")
false
}
}
internal val buttonVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlButton> { name, client, vision, _ ->
button(type = ButtonType.button).also { button ->
button.subscribeToVision(vision)
button.onclick = {
client.context.launch {
client.sendEvent(name, VisionClickEvent(name = name))
}
}
vision.useProperty(VisionOfHtmlButton::label) {
button.innerHTML = it ?: ""
}
}
}

View File

@ -1,18 +1,18 @@
package space.kscience.visionforge
import kotlinx.browser.document
import kotlinx.coroutines.launch
import kotlinx.dom.clear
import kotlinx.html.InputType
import kotlinx.html.div
import kotlinx.html.js.input
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
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.*
import space.kscience.dataforge.meta.Value
import space.kscience.dataforge.meta.asValue
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.*
/**
@ -20,13 +20,19 @@ import space.kscience.visionforge.html.*
*
* @param vision The vision to subscribe to.
*/
private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
internal fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
vision.useProperty(VisionOfHtml::classes) {
classList.value = classes.joinToString(separator = " ")
}
}
private fun VisionClient.sendInputEvent(name: Name, value: Value?) {
context.launch {
sendEvent(name, VisionValueChangeEvent(value, name))
}
}
/**
* Subscribes the HTML input element to a given vision.
*
@ -40,46 +46,48 @@ private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
}
internal val htmlVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfPlainHtml> { _, vision, _ ->
div {}.also { div ->
ElementVisionRenderer<VisionOfPlainHtml> { _, _, vision, _ ->
div().also { div ->
div.subscribeToVision(vision)
vision.useProperty(VisionOfPlainHtml::content) {
div.textContent = it
div.clear()
if (it != null) div.innerHTML = it
}
}
}
internal val inputVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlInput>(acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1) { _, vision, _ ->
input {
type = InputType.text
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
vision.value = htmlInputElement.value.asValue()
}
internal val inputVisionRenderer: ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlInput>(
acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1
) { name, client, vision, _ ->
input {
type = InputType.text
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
client.sendInputEvent(name, htmlInputElement.value.asValue())
}
when (vision.feedbackMode) {
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
when (vision.feedbackMode) {
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
InputFeedbackMode.NONE -> {}
}
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
InputFeedbackMode.NONE -> {}
}
htmlInputElement.subscribeToInput(vision)
vision.useProperty(VisionOfHtmlInput::value) {
htmlInputElement.value = it?.string ?: ""
}
htmlInputElement.subscribeToInput(vision)
vision.useProperty(VisionOfHtmlInput::value) {
htmlInputElement.value = it?.string ?: ""
}
}
}
internal val checkboxVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfCheckbox> { _, vision, _ ->
ElementVisionRenderer<VisionOfCheckbox> { name, client, vision, _ ->
input {
type = InputType.checkBox
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
vision.checked = htmlInputElement.checked
client.sendInputEvent(name, htmlInputElement.checked.asValue())
}
@ -98,12 +106,12 @@ internal val checkboxVisionRenderer: ElementVisionRenderer =
}
internal val textVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfTextField> { _, vision, _ ->
ElementVisionRenderer<VisionOfTextField> { name, client, vision, _ ->
input {
type = InputType.text
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
vision.text = htmlInputElement.value
client.sendInputEvent(name, htmlInputElement.value.asValue())
}
@ -122,13 +130,15 @@ internal val textVisionRenderer: ElementVisionRenderer =
}
internal val numberVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfNumberField> { _, vision, _ ->
ElementVisionRenderer<VisionOfNumberField> { name, client, vision, _ ->
input {
type = InputType.text
type = InputType.number
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it }
htmlInputElement.value.toDoubleOrNull()?.let {
client.sendInputEvent(name, htmlInputElement.value.asValue())
}
}
when (vision.feedbackMode) {
@ -145,16 +155,18 @@ internal val numberVisionRenderer: ElementVisionRenderer =
}
internal val rangeVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfRangeField> { _, vision, _ ->
ElementVisionRenderer<VisionOfRangeField> { name, client, vision, _ ->
input {
type = InputType.text
type = InputType.range
min = vision.min.toString()
max = vision.max.toString()
step = vision.step.toString()
}.also { htmlInputElement ->
val onEvent: (Event) -> Unit = {
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it }
htmlInputElement.value.toDoubleOrNull()?.let {
client.sendInputEvent(name, htmlInputElement.value.asValue())
}
}
when (vision.feedbackMode) {
@ -169,53 +181,3 @@ internal val rangeVisionRenderer: ElementVisionRenderer =
}
}
}
internal fun FormData.toMeta(): Meta {
@Suppress("UNUSED_VARIABLE") val formData = this
//val res = js("Object.fromEntries(formData);")
val `object` = js("{}")
//language=JavaScript
js(
"""
formData.forEach(function(value, key){
// Reflect.has in favor of: object.hasOwnProperty(key)
if(!Reflect.has(object, key)){
object[key] = value;
return;
}
if(!Array.isArray(object[key])){
object[key] = [object[key]];
}
object[key].push(value);
});
"""
)
return DynamicMeta(`object`)
}
internal val formVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlForm> { _, vision, _ ->
val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form")
form.subscribeToVision(vision)
vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" }
vision.useProperty(VisionOfHtmlForm::values) { values ->
vision.manager?.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()
vision.values = formData
console.info("Sent: ${formData.toMap()}")
false
}
}

View File

@ -27,7 +27,7 @@ public actual class MarkupPlugin : VisionPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
require(vision is VisionOfMarkup) { "The vision is not a markup vision" }
val div = document.createElement("div")
val flavour = when (vision.format) {

View File

@ -10,10 +10,7 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.plotly.PlotlyConfig
import space.kscience.plotly.plot
import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.JsVisionClient
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionPlugin
import space.kscience.visionforge.*
public actual class PlotlyPlugin : VisionPlugin(), ElementVisionRenderer {
public val visionClient: JsVisionClient by require(JsVisionClient)
@ -27,7 +24,7 @@ public actual class PlotlyPlugin : VisionPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
val plot = (vision as? VisionOfPlotly)?.plot ?: error("VisionOfPlotly expected but ${vision::class} found")
val config = PlotlyConfig.read(meta)
element.plot(config, plot)

View File

@ -14,6 +14,7 @@ import space.kscience.dataforge.names.asName
import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.JsVisionClient
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionClient
import tabulator.Tabulator
import tabulator.TabulatorFull
@ -34,7 +35,7 @@ public class TableVisionJsPlugin : AbstractPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
val table: VisionOfTable = (vision as? VisionOfTable)
?: error("VisionOfTable expected but ${vision::class} found")

View File

@ -150,7 +150,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
render(vision)
}
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
renderSolid(
element,
vision as? Solid ?: error("Solid expected but ${vision::class} found"),