Implement and test input elements
This commit is contained in:
parent
05b87857f4
commit
f40bed7bb9
59
demo/playground/src/jvmMain/kotlin/controlVision.kt
Normal file
59
demo/playground/src/jvmMain/kotlin/controlVision.kt
Normal 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)
|
||||||
|
}
|
@ -8,14 +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
|
||||||
import space.kscience.dataforge.meta.Value
|
|
||||||
|
|
||||||
@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
|
||||||
@ -24,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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,7 +74,18 @@ public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClic
|
|||||||
public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent() {
|
public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent() {
|
||||||
|
|
||||||
public val value: Value? get() = meta.value
|
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?): VisionValueChangeEvent =
|
public fun VisionValueChangeEvent(value: Value?, name: Name? = null): VisionValueChangeEvent = VisionValueChangeEvent(
|
||||||
VisionValueChangeEvent(Meta { this.value = value })
|
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
|
||||||
|
|
||||||
@ -32,10 +30,4 @@ 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.sendMetaEvent(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,9 +1,11 @@
|
|||||||
package space.kscience.visionforge.html
|
package space.kscience.visionforge.html
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
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
|
||||||
@ -27,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")
|
||||||
@ -54,15 +58,7 @@ public enum class InputFeedbackMode {
|
|||||||
NONE
|
NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
public abstract class VisionOfHtmlControl: VisionOfHtml(), ControlVision{
|
||||||
@SerialName("html.input")
|
|
||||||
public open class VisionOfHtmlInput(
|
|
||||||
public val inputType: String,
|
|
||||||
public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE,
|
|
||||||
) : VisionOfHtml(), ControlVision {
|
|
||||||
public var value: Value? by properties.value()
|
|
||||||
public var disabled: Boolean by properties.boolean { false }
|
|
||||||
public var fieldName: String? by properties.string()
|
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private val mutableControlEventFlow = MutableSharedFlow<VisionControlEvent>()
|
private val mutableControlEventFlow = MutableSharedFlow<VisionControlEvent>()
|
||||||
@ -70,14 +66,32 @@ public open class VisionOfHtmlInput(
|
|||||||
override val controlEventFlow: SharedFlow<VisionControlEvent>
|
override val controlEventFlow: SharedFlow<VisionControlEvent>
|
||||||
get() = mutableControlEventFlow
|
get() = mutableControlEventFlow
|
||||||
|
|
||||||
override fun dispatchControlEvent(event: VisionControlEvent) {
|
override suspend fun dispatchControlEvent(event: VisionControlEvent) {
|
||||||
if(event is VisionValueChangeEvent){
|
mutableControlEventFlow.emit(event)
|
||||||
this.value = event.value
|
|
||||||
}
|
|
||||||
mutableControlEventFlow.tryEmit(event)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("html.input")
|
||||||
|
public open class VisionOfHtmlInput(
|
||||||
|
public val inputType: String,
|
||||||
|
public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE,
|
||||||
|
) : 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")
|
@Suppress("UnusedReceiverParameter")
|
||||||
public inline fun VisionOutput.htmlInput(
|
public inline fun VisionOutput.htmlInput(
|
||||||
inputType: String,
|
inputType: String,
|
||||||
@ -110,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")
|
||||||
@ -125,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,4 +28,21 @@ public fun <R> TagConsumer<R>.bindForm(
|
|||||||
): R = form {
|
): R = form {
|
||||||
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()
|
||||||
}
|
}
|
@ -135,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" }
|
||||||
}
|
}
|
||||||
@ -262,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,21 +1,17 @@
|
|||||||
package space.kscience.visionforge
|
package space.kscience.visionforge
|
||||||
|
|
||||||
import kotlinx.browser.document
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.dom.clear
|
import kotlinx.dom.clear
|
||||||
import kotlinx.html.InputType
|
import kotlinx.html.InputType
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.dom.append
|
|
||||||
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.dataforge.names.Name
|
||||||
import space.kscience.visionforge.html.*
|
import space.kscience.visionforge.html.*
|
||||||
|
|
||||||
@ -24,7 +20,7 @@ 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 = " ")
|
||||||
}
|
}
|
||||||
@ -33,7 +29,7 @@ private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
|
|||||||
|
|
||||||
private fun VisionClient.sendInputEvent(name: Name, value: Value?) {
|
private fun VisionClient.sendInputEvent(name: Name, value: Value?) {
|
||||||
context.launch {
|
context.launch {
|
||||||
sendEvent(name, VisionValueChangeEvent(value))
|
sendEvent(name, VisionValueChangeEvent(value, name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,43 +47,39 @@ 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.clear()
|
div.clear()
|
||||||
div.append {
|
if (it != null) div.innerHTML = it
|
||||||
|
|
||||||
}
|
|
||||||
div.textContent = it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val inputVisionRenderer: ElementVisionRenderer =
|
internal val inputVisionRenderer: ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlInput>(
|
||||||
ElementVisionRenderer<VisionOfHtmlInput>(
|
acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1
|
||||||
acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1
|
) { name, client, vision, _ ->
|
||||||
) { name, client, vision, _ ->
|
input {
|
||||||
input {
|
type = InputType.text
|
||||||
type = InputType.text
|
}.also { htmlInputElement ->
|
||||||
}.also { htmlInputElement ->
|
val onEvent: (Event) -> Unit = {
|
||||||
val onEvent: (Event) -> Unit = {
|
client.sendInputEvent(name, htmlInputElement.value.asValue())
|
||||||
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> { name, client, vision, _ ->
|
ElementVisionRenderer<VisionOfCheckbox> { name, client, vision, _ ->
|
||||||
@ -95,7 +87,7 @@ internal val checkboxVisionRenderer: ElementVisionRenderer =
|
|||||||
type = InputType.checkBox
|
type = InputType.checkBox
|
||||||
}.also { htmlInputElement ->
|
}.also { htmlInputElement ->
|
||||||
val onEvent: (Event) -> Unit = {
|
val onEvent: (Event) -> Unit = {
|
||||||
client.sendInputEvent(name, htmlInputElement.value.asValue())
|
client.sendInputEvent(name, htmlInputElement.checked.asValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -140,7 +132,7 @@ internal val textVisionRenderer: ElementVisionRenderer =
|
|||||||
internal val numberVisionRenderer: ElementVisionRenderer =
|
internal val numberVisionRenderer: ElementVisionRenderer =
|
||||||
ElementVisionRenderer<VisionOfNumberField> { name, client, 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 = {
|
||||||
@ -165,7 +157,7 @@ internal val numberVisionRenderer: ElementVisionRenderer =
|
|||||||
internal val rangeVisionRenderer: ElementVisionRenderer =
|
internal val rangeVisionRenderer: ElementVisionRenderer =
|
||||||
ElementVisionRenderer<VisionOfRangeField> { name, client, 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()
|
||||||
@ -189,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> { 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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user