Compare commits
3 Commits
9fc6f1e34c
...
f40bed7bb9
Author | SHA1 | Date | |
---|---|---|---|
f40bed7bb9 | |||
05b87857f4 | |||
bce61c0fb0 |
@ -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 {
|
||||
|
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)
|
||||
}
|
@ -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) {
|
||||
|
@ -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()) }
|
||||
}
|
||||
)
|
||||
|
@ -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" }
|
||||
}
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user