Html input events

This commit is contained in:
Alexander Nozik 2023-12-05 18:50:26 +03:00
parent 9fc6f1e34c
commit bce61c0fb0
7 changed files with 67 additions and 22 deletions

View File

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

View File

@ -11,6 +11,7 @@ import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaRepr import space.kscience.dataforge.meta.MetaRepr
import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.Value
@Serializable @Serializable
@SerialName("control") @SerialName("control")
@ -60,4 +61,10 @@ 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
}
public fun VisionValueChangeEvent(value: Value?): VisionValueChangeEvent =
VisionValueChangeEvent(Meta { this.value = value })

View File

@ -34,7 +34,7 @@ public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: St
notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
} }
public fun VisionClient.sendEvent(targetName: Name, payload: MetaRepr): Unit { public fun VisionClient.sendMetaEvent(targetName: Name, payload: MetaRepr): Unit {
context.launch { context.launch {
sendEvent(targetName, VisionMetaEvent(payload.toMeta())) sendEvent(targetName, VisionMetaEvent(payload.toMeta()))
} }

View File

@ -1,13 +1,19 @@
package space.kscience.visionforge.html package space.kscience.visionforge.html
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.html.InputType import kotlinx.html.InputType
import kotlinx.html.TagConsumer import kotlinx.html.TagConsumer
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
@ -53,10 +59,23 @@ public enum class InputFeedbackMode {
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() { ) : VisionOfHtml(), ControlVision {
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()
@Transient
private val mutableControlEventFlow = MutableSharedFlow<VisionControlEvent>()
override val controlEventFlow: SharedFlow<VisionControlEvent>
get() = mutableControlEventFlow
override fun dispatchControlEvent(event: VisionControlEvent) {
if(event is VisionValueChangeEvent){
this.value = event.value
}
mutableControlEventFlow.tryEmit(event)
}
} }
@Suppress("UnusedReceiverParameter") @Suppress("UnusedReceiverParameter")

View File

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

View File

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

View File

@ -40,7 +40,7 @@ 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) {
@ -50,12 +50,14 @@ internal val htmlVisionRenderer: ElementVisionRenderer =
} }
internal val inputVisionRenderer: ElementVisionRenderer = internal val inputVisionRenderer: ElementVisionRenderer =
ElementVisionRenderer<VisionOfHtmlInput>(acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1) { _, vision, _ -> ElementVisionRenderer<VisionOfHtmlInput>(
acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1
) { name, client, vision, _ ->
input { input {
type = InputType.text type = InputType.text
}.also { htmlInputElement -> }.also { htmlInputElement ->
val onEvent: (Event) -> Unit = { val onEvent: (Event) -> Unit = {
vision.value = htmlInputElement.value.asValue() client.sendEvent(name, VisionValueChangeEvent(htmlInputElement.value.asValue()))
} }
@ -74,12 +76,12 @@ internal val inputVisionRenderer: ElementVisionRenderer =
} }
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.sendEvent(name, VisionValueChangeEvent(htmlInputElement.checked.asValue()))
} }
@ -98,12 +100,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.sendEvent(name, VisionValueChangeEvent(htmlInputElement.value.asValue()))
} }
@ -122,13 +124,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.text
}.also { htmlInputElement -> }.also { htmlInputElement ->
val onEvent: (Event) -> Unit = { val onEvent: (Event) -> Unit = {
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it } htmlInputElement.value.toDoubleOrNull()?.let {
client.sendEvent(name, VisionValueChangeEvent(it.asValue()))
}
} }
when (vision.feedbackMode) { when (vision.feedbackMode) {
@ -145,7 +149,7 @@ 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.text
min = vision.min.toString() min = vision.min.toString()
@ -154,7 +158,9 @@ internal val rangeVisionRenderer: ElementVisionRenderer =
}.also { htmlInputElement -> }.also { htmlInputElement ->
val onEvent: (Event) -> Unit = { val onEvent: (Event) -> Unit = {
htmlInputElement.value.toDoubleOrNull()?.let { vision.number = it } htmlInputElement.value.toDoubleOrNull()?.let {
client.sendEvent(name, VisionValueChangeEvent(it.asValue()))
}
} }
when (vision.feedbackMode) { when (vision.feedbackMode) {