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
build.gradle.kts
demo/playground/src/jvmMain/kotlin
ui/ring/src/jsMain/kotlin/space.kscience.visionforge.ring
visionforge-core/src
visionforge-markdown/src/jsMain/kotlin/space/kscience/visionforge/markup
visionforge-plotly/src/jsMain/kotlin/space/kscience/visionforge/plotly
visionforge-tables/src/jsMain/kotlin/space/kscience/visionforge/tables
visionforge-threejs/src/jsMain/kotlin/space/kscience/visionforge/solid/three

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

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

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

@ -8,13 +8,13 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.MetaRepr import space.kscience.dataforge.names.Name
import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.names.parseAsName
@Serializable @Serializable
@SerialName("control") @SerialName("control")
public abstract class VisionControlEvent : VisionEvent, MetaRepr { public sealed class VisionControlEvent : VisionEvent, MetaRepr {
public abstract val meta: Meta public abstract val meta: Meta
override fun toMeta(): Meta = meta override fun toMeta(): Meta = meta
@ -23,30 +23,41 @@ public abstract class VisionControlEvent : VisionEvent, MetaRepr {
public interface ControlVision : Vision { public interface ControlVision : Vision {
public val controlEventFlow: SharedFlow<VisionControlEvent> 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) { if (event is VisionControlEvent) {
dispatchControlEvent(event) dispatchControlEvent(event)
} else super.receiveEvent(event) } else super.receiveEvent(event)
} }
} }
/** /**
* @param payload The optional payload associated with the click event. * @param payload The optional payload associated with the click event.
*/ */
@Serializable @Serializable
@SerialName("control.click") @SerialName("control.click")
public class VisionClickEvent(public val payload: Meta = Meta.EMPTY) : VisionControlEvent() { public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() {
override val meta: Meta get() = Meta { ::payload.name put payload } 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 { public interface ClickControl : ControlVision {
/** /**
* Create and dispatch a click event * Create and dispatch a click event
*/ */
public fun click(builder: MutableMeta.() -> Unit = {}) { public suspend fun click(builder: MutableMeta.() -> Unit = {}) {
dispatchControlEvent(VisionClickEvent(Meta(builder))) dispatchControlEvent(VisionClickEvent(Meta(builder)))
} }
} }
@ -60,4 +71,21 @@ public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClic
@Serializable @Serializable
@SerialName("control.valueChange") @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()) }
}
)

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

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

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

@ -1,13 +1,21 @@
package space.kscience.visionforge.html 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.InputType
import kotlinx.html.TagConsumer import kotlinx.html.div
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.visionforge.AbstractVision import space.kscience.visionforge.AbstractVision
import space.kscience.visionforge.ControlVision
import space.kscience.visionforge.VisionControlEvent
import space.kscience.visionforge.VisionValueChangeEvent
@Serializable @Serializable
@ -21,8 +29,10 @@ public class VisionOfPlainHtml : VisionOfHtml() {
public var content: String? by properties.string() public var content: String? by properties.string()
} }
public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) { public fun VisionOfPlainHtml.content(block: DIV.() -> Unit) {
content = createHTML().apply(block).finalize() content = createHTML().apply {
div(block = block)
}.finalize()
} }
@Suppress("UnusedReceiverParameter") @Suppress("UnusedReceiverParameter")
@ -48,17 +58,40 @@ public enum class InputFeedbackMode {
NONE 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 @Serializable
@SerialName("html.input") @SerialName("html.input")
public open class VisionOfHtmlInput( public open class VisionOfHtmlInput(
public val inputType: String, public val inputType: String,
public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE, public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE,
) : VisionOfHtml() { ) : VisionOfHtmlControl() {
public var value: Value? by properties.value() public var value: Value? by properties.value()
public var disabled: Boolean by properties.boolean { false } public var disabled: Boolean by properties.boolean { false }
public var fieldName: String? by properties.string() 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") @Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlInput( public inline fun VisionOutput.htmlInput(
inputType: String, inputType: String,
@ -91,7 +124,7 @@ public inline fun VisionOutput.htmlCheckBox(
@Serializable @Serializable
@SerialName("html.number") @SerialName("html.number")
public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) { 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") @Suppress("UnusedReceiverParameter")
@ -106,14 +139,14 @@ public class VisionOfRangeField(
public val max: Double, public val max: Double,
public val step: Double = 1.0, public val step: Double = 1.0,
) : VisionOfHtmlInput(InputType.range.realValue) { ) : 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") @Suppress("UnusedReceiverParameter")
public inline fun VisionOutput.htmlRangeField( public inline fun VisionOutput.htmlRangeField(
min: Double, min: Number,
max: Double, max: Number,
step: Double = 1.0, step: Number = 1.0,
block: VisionOfRangeField.() -> Unit = {}, block: VisionOfRangeField.() -> Unit = {},
): VisionOfRangeField = VisionOfRangeField(min, max, step).apply(block) ): VisionOfRangeField = VisionOfRangeField(min.toDouble(), max.toDouble(), step.toDouble()).apply(block)

@ -8,6 +8,8 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable 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
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 * @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") @SerialName("html.form")
public class VisionOfHtmlForm( public class VisionOfHtmlForm(
public val formId: String, public val formId: String,
) : VisionOfHtml() { ) : VisionOfHtmlControl() {
public var values: Meta? by properties.node() public var values: Meta? by properties.node()
} }
@ -27,3 +29,20 @@ public fun <R> TagConsumer<R>.bindForm(
this.id = visionOfForm.formId this.id = visionOfForm.formId
builder() 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()
}

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

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

@ -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 ?: ""
}
}
}

@ -1,18 +1,18 @@
package space.kscience.visionforge package space.kscience.visionforge
import kotlinx.browser.document import kotlinx.coroutines.launch
import kotlinx.dom.clear
import kotlinx.html.InputType import kotlinx.html.InputType
import kotlinx.html.div import kotlinx.html.div
import kotlinx.html.js.input import kotlinx.html.js.input
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
import org.w3c.dom.get import space.kscience.dataforge.meta.Value
import org.w3c.xhr.FormData import space.kscience.dataforge.meta.asValue
import space.kscience.dataforge.context.debug import space.kscience.dataforge.meta.double
import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.string
import space.kscience.dataforge.meta.* import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.* import space.kscience.visionforge.html.*
/** /**
@ -20,13 +20,19 @@ import space.kscience.visionforge.html.*
* *
* @param vision The vision to subscribe to. * @param vision The vision to subscribe to.
*/ */
private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { internal fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
vision.useProperty(VisionOfHtml::classes) { vision.useProperty(VisionOfHtml::classes) {
classList.value = classes.joinToString(separator = " ") 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. * Subscribes the HTML input element to a given vision.
* *
@ -40,46 +46,48 @@ private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
} }
internal val htmlVisionRenderer: ElementVisionRenderer = internal val htmlVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfPlainHtml> { _, vision, _ -> ElementVisionRenderer<VisionOfPlainHtml> { _, _, vision, _ ->
div {}.also { div -> div().also { div ->
div.subscribeToVision(vision) div.subscribeToVision(vision)
vision.useProperty(VisionOfPlainHtml::content) { vision.useProperty(VisionOfPlainHtml::content) {
div.textContent = it div.clear()
if (it != null) div.innerHTML = it
} }
} }
} }
internal val inputVisionRenderer: ElementVisionRenderer = internal val inputVisionRenderer: ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlInput>(
ElementVisionRenderer<VisionOfHtmlInput>(acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1) { _, vision, _ -> acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1
input { ) { name, client, vision, _ ->
type = InputType.text input {
}.also { htmlInputElement -> type = InputType.text
val onEvent: (Event) -> Unit = { }.also { htmlInputElement ->
vision.value = htmlInputElement.value.asValue() val onEvent: (Event) -> Unit = {
} client.sendInputEvent(name, htmlInputElement.value.asValue())
}
when (vision.feedbackMode) { when (vision.feedbackMode) {
InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent
InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent
InputFeedbackMode.NONE -> {} InputFeedbackMode.NONE -> {}
} }
htmlInputElement.subscribeToInput(vision) htmlInputElement.subscribeToInput(vision)
vision.useProperty(VisionOfHtmlInput::value) { vision.useProperty(VisionOfHtmlInput::value) {
htmlInputElement.value = it?.string ?: "" htmlInputElement.value = it?.string ?: ""
}
} }
} }
}
internal val checkboxVisionRenderer: ElementVisionRenderer = internal val checkboxVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfCheckbox> { _, vision, _ -> ElementVisionRenderer<VisionOfCheckbox> { name, client, vision, _ ->
input { input {
type = InputType.checkBox type = InputType.checkBox
}.also { htmlInputElement -> }.also { htmlInputElement ->
val onEvent: (Event) -> Unit = { 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 = internal val textVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfTextField> { _, vision, _ -> ElementVisionRenderer<VisionOfTextField> { name, client, vision, _ ->
input { input {
type = InputType.text type = InputType.text
}.also { htmlInputElement -> }.also { htmlInputElement ->
val onEvent: (Event) -> Unit = { 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 = internal val numberVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfNumberField> { _, vision, _ -> ElementVisionRenderer<VisionOfNumberField> { name, client, vision, _ ->
input { input {
type = InputType.text type = InputType.number
}.also { htmlInputElement -> }.also { htmlInputElement ->
val onEvent: (Event) -> Unit = { val onEvent: (Event) -> Unit = {
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it } htmlInputElement.value.toDoubleOrNull()?.let {
client.sendInputEvent(name, htmlInputElement.value.asValue())
}
} }
when (vision.feedbackMode) { when (vision.feedbackMode) {
@ -145,16 +155,18 @@ internal val numberVisionRenderer: ElementVisionRenderer =
} }
internal val rangeVisionRenderer: ElementVisionRenderer = internal val rangeVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfRangeField> { _, vision, _ -> ElementVisionRenderer<VisionOfRangeField> { name, client, vision, _ ->
input { input {
type = InputType.text type = InputType.range
min = vision.min.toString() min = vision.min.toString()
max = vision.max.toString() max = vision.max.toString()
step = vision.step.toString() step = vision.step.toString()
}.also { htmlInputElement -> }.also { htmlInputElement ->
val onEvent: (Event) -> Unit = { val onEvent: (Event) -> Unit = {
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it } htmlInputElement.value.toDoubleOrNull()?.let {
client.sendInputEvent(name, htmlInputElement.value.asValue())
}
} }
when (vision.feedbackMode) { 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
}
}

@ -27,7 +27,7 @@ public actual class MarkupPlugin : VisionPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING 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" } require(vision is VisionOfMarkup) { "The vision is not a markup vision" }
val div = document.createElement("div") val div = document.createElement("div")
val flavour = when (vision.format) { val flavour = when (vision.format) {

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

@ -14,6 +14,7 @@ import space.kscience.dataforge.names.asName
import space.kscience.visionforge.ElementVisionRenderer import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.JsVisionClient import space.kscience.visionforge.JsVisionClient
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionClient
import tabulator.Tabulator import tabulator.Tabulator
import tabulator.TabulatorFull import tabulator.TabulatorFull
@ -34,7 +35,7 @@ public class TableVisionJsPlugin : AbstractPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING 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) val table: VisionOfTable = (vision as? VisionOfTable)
?: error("VisionOfTable expected but ${vision::class} found") ?: error("VisionOfTable expected but ${vision::class} found")

@ -150,7 +150,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
render(vision) 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( renderSolid(
element, element,
vision as? Solid ?: error("Solid expected but ${vision::class} found"), vision as? Solid ?: error("Solid expected but ${vision::class} found"),