diff --git a/build.gradle.kts b/build.gradle.kts index b7ee4ffd..95821d28 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,11 @@ -import scientifik.fx -import scientifik.serialization - -val dataforgeVersion by extra("0.1.7") +val dataforgeVersion by extra("0.1.8-dev-2") plugins { - val toolsVersion = "0.4.2" + val toolsVersion = "0.5.0" id("scientifik.mpp") version toolsVersion apply false id("scientifik.jvm") version toolsVersion apply false id("scientifik.js") version toolsVersion apply false id("scientifik.publish") version toolsVersion apply false - id("org.openjfx.javafxplugin") version "0.0.8" apply false } allprojects { @@ -19,12 +15,13 @@ allprojects { maven("http://maven.jzy3d.org/releases") maven("https://kotlin.bintray.com/js-externals") maven("https://kotlin.bintray.com/kotlin-js-wrappers/") + maven("https://dl.bintray.com/mipt-npm/dataforge") // maven("https://dl.bintray.com/gbaldeck/kotlin") // maven("https://dl.bintray.com/rjaros/kotlin") } group = "hep.dataforge" - version = "0.1.3-dev" + version = "0.1.4-dev" } val githubProject by extra("dataforge-vis") @@ -32,9 +29,9 @@ val bintrayRepo by extra("dataforge") val fxVersion by extra("14") subprojects { - apply(plugin = "scientifik.publish") - serialization() - afterEvaluate { - fx(scientifik.FXModule.CONTROLS, version = fxVersion) + if(name.startsWith("dataforge")) { + apply(plugin = "scientifik.publish") } + useSerialization() + useFx(FXModule.CONTROLS, version = fxVersion) } \ No newline at end of file diff --git a/dataforge-vis-common/build.gradle.kts b/dataforge-vis-common/build.gradle.kts index 11236809..7ebb7feb 100644 --- a/dataforge-vis-common/build.gradle.kts +++ b/dataforge-vis-common/build.gradle.kts @@ -32,19 +32,23 @@ kotlin { dependencies { api("hep.dataforge:dataforge-output-html:$dataforgeVersion") - //React, React DOM + Wrappers (chapter 3) - api("org.jetbrains:kotlin-react:16.13.0-pre.94-kotlin-1.3.70") - api("org.jetbrains:kotlin-react-dom:16.13.0-pre.94-kotlin-1.3.70") - api(npm("react", "16.13.0")) - api(npm("react-dom", "16.13.0")) + api("org.jetbrains:kotlin-react:16.13.1-pre.104-kotlin-1.3.72") + api("org.jetbrains:kotlin-react-dom:16.13.1-pre.104-kotlin-1.3.72") + api("org.jetbrains.kotlinx:kotlinx-html:0.6.12") - //Kotlin Styled (chapter 3) - api("org.jetbrains:kotlin-styled:1.0.0-pre.94-kotlin-1.3.70") - api(npm("styled-components")) - api(npm("inline-style-prefixer")) + api("org.jetbrains:kotlin-extensions:1.0.1-pre.104-kotlin-1.3.72") + api("org.jetbrains:kotlin-css-js:1.0.0-pre.94-kotlin-1.3.70") + api("org.jetbrains:kotlin-styled:1.0.0-pre.104-kotlin-1.3.72") - api(npm("source-map-resolve","0.6.0")) - api(npm("file-saver","2.0.2")) + api(npm("core-js", "2.6.5")) + + api(npm("react", "16.13.1")) + api(npm("react-dom", "16.13.1")) + + api(npm("react-is", "16.13.0")) + api(npm("inline-style-prefixer", "5.1.0")) + api(npm("styled-components", "4.3.2")) + //api(project(":ringui-wrapper")) } } } diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/AbstractVisualObject.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/AbstractVisualObject.kt index 259a5e3a..d57e20b3 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/AbstractVisualObject.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/AbstractVisualObject.kt @@ -83,20 +83,34 @@ abstract class AbstractVisualObject : VisualObject { override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { return if (inherit) { - properties?.get(name) ?: mergedStyles[name] ?: parent?.getProperty(name, inherit) + sequence { + yield(properties?.get(name)) + yield(mergedStyles[name]) + yield(parent?.getProperty(name, inherit)) + }.merge() } else { - properties?.get(name) ?: mergedStyles[name] + sequence { + yield(properties?.get(name)) + yield(mergedStyles[name]) + }.merge() } } + /** + * Reset all properties to their default values + */ + fun resetProperties() { + properties?.removeListener(this) + properties = null + } + companion object { val descriptor = NodeDescriptor { - defineValue(STYLE_KEY){ + value(STYLE_KEY) { type(ValueType.STRING) multiple = true } } - } } diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/VisualObject.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/VisualObject.kt index b757fcb3..bab25b1f 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/VisualObject.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/VisualObject.kt @@ -44,6 +44,9 @@ interface VisualObject : Configurable { */ fun propertyChanged(name: Name, before: MetaItem<*>?, after: MetaItem<*>?): Unit + /** + * Send a signal that property value should be reevaluated + */ fun propertyInvalidated(name: Name) = propertyChanged(name, null, null) /** diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/misc.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/misc.kt index 756fd42e..f6ccc200 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/misc.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/misc.kt @@ -1,5 +1,8 @@ package hep.dataforge.vis +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.node import hep.dataforge.names.Name import hep.dataforge.names.isEmpty @@ -16,4 +19,16 @@ tailrec fun Name.selectable(): Name? = when { else -> { cutLast().selectable() } +} + +fun Sequence?>.merge(): MetaItem<*>?{ + return when (val first = filterNotNull().firstOrNull()) { + null -> null + is MetaItem.ValueItem -> first //fast search for first entry if it is value + is MetaItem.NodeItem -> { + //merge nodes if first encountered node is meta + val laminate: Laminate = Laminate(mapNotNull { it.node }.toList()) + MetaItem.NodeItem(laminate) + } + } } \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/valueWidget.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/valueWidget.kt index 80e44a35..7227a7f0 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/valueWidget.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/valueWidget.kt @@ -2,22 +2,25 @@ package hep.dataforge.vis import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.ValueDescriptor +import hep.dataforge.meta.descriptors.attributes +import hep.dataforge.meta.descriptors.setAttribute +import hep.dataforge.names.toName import hep.dataforge.values.asValue /** * Extension property to access the "widget" key of [ValueDescriptor] */ var ValueDescriptor.widget: Meta - get() = getProperty("widget").node ?: Meta.EMPTY + get() = attributes["widget"].node ?: Meta.EMPTY set(value) { - setProperty("widget", value) + setAttribute("widget".toName(), value) } /** * Extension property to access the "widget.type" key of [ValueDescriptor] */ var ValueDescriptor.widgetType: String? - get() = getProperty("widget.type").string + get() = attributes["widget.type"].string set(value) { - setProperty("widget.type", value?.asValue()) + setAttribute("widget.type".toName(), value) } \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/jsExtra.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/jsExtra.kt index 6354a61c..e3105dd5 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/jsExtra.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/jsExtra.kt @@ -12,10 +12,6 @@ inline fun jsObject(builder: T.() -> Unit): T { inline fun js(builder: dynamic.() -> Unit): dynamic = jsObject(builder) -//fun clone(obj: T) = objectAssign(jsObject {}, obj) - -//inline fun assign(obj: T, builder: T.() -> Unit) = clone(obj).apply(builder) - fun toPlainObjectStripNull(obj: Any) = js { for (key in Object.keys(obj)) { val value = obj.asDynamic()[key] diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt deleted file mode 100644 index 9b71a03a..00000000 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChooser.kt +++ /dev/null @@ -1,90 +0,0 @@ -package hep.dataforge.vis.editor - -import hep.dataforge.meta.Config -import hep.dataforge.meta.descriptors.ValueDescriptor -import hep.dataforge.meta.get -import hep.dataforge.meta.setValue -import hep.dataforge.meta.string -import hep.dataforge.names.Name -import hep.dataforge.values.* -import hep.dataforge.vis.widgetType -import kotlinx.html.InputType -import kotlinx.html.js.onChangeFunction -import org.w3c.dom.HTMLInputElement -import org.w3c.dom.HTMLSelectElement -import org.w3c.dom.events.Event -import react.RBuilder -import react.dom.* - -internal fun RBuilder.valueChooser(root: Config, name: Name, value: Value, descriptor: ValueDescriptor?) { - val onValueChange: (Event) -> Unit = { - val res = when (val t = it.target) { - // (it.target as HTMLInputElement).value - is HTMLInputElement -> if (t.type == "checkbox") { - if (t.checked) True else False - } else { - t.value.asValue() - } - is HTMLSelectElement -> t.value.asValue() - else -> error("Unknown event target: $t") - } - - try { - root.setValue(name, res) - } catch (ex: Exception) { - console.error("Can't set config property ${name} to $res") - } - } - - div() { - val type = descriptor?.type?.firstOrNull() - when { - type == ValueType.BOOLEAN -> { - input(type = InputType.checkBox) { - attrs { - checked = value.boolean - onChangeFunction = onValueChange - } - } - } - type == ValueType.NUMBER -> input(type = InputType.number, classes = "form-control w-100") { - attrs { - descriptor.attributes["step"].string?.let { - step = it - } - descriptor.attributes["min"].string?.let { - min = it - } - descriptor.attributes["max"].string?.let { - max = it - } - this.defaultValue = value.string - onChangeFunction = onValueChange - } - } - descriptor?.allowedValues?.isNotEmpty() ?: false -> select (classes = "w-100") { - descriptor!!.allowedValues.forEach { - option { - +it.string - } - } - attrs { - multiple = false - onChangeFunction = onValueChange - } - } - descriptor?.widgetType == "color" -> input(type = InputType.color) { - attrs { - this.value = value.string - onChangeFunction = onValueChange - } - } - else -> input(type = InputType.text, classes = "form-control w-100") { - attrs { - this.value = value.string - onChangeFunction = onValueChange - } - } - } - } -} \ No newline at end of file diff --git a/dataforge-vis-common/src/jvmTest/kotlin/hep/dataforge/vis/demo/MetaEditorDemo.kt b/dataforge-vis-common/src/jvmTest/kotlin/hep/dataforge/vis/demo/MetaEditorDemo.kt index 0c4060f1..f41a9d41 100644 --- a/dataforge-vis-common/src/jvmTest/kotlin/hep/dataforge/vis/demo/MetaEditorDemo.kt +++ b/dataforge-vis-common/src/jvmTest/kotlin/hep/dataforge/vis/demo/MetaEditorDemo.kt @@ -26,21 +26,21 @@ class MetaEditorDemo : View("Meta editor demo") { }.asConfig() val descriptor = NodeDescriptor { - defineNode("aNode") { + node("aNode") { info = "A root demo node" - defineValue("b") { + value("b") { info = "b number value" type(ValueType.NUMBER) } - defineNode("otherNode") { - defineValue("otherValue") { + node("otherNode") { + value("otherValue") { type(ValueType.BOOLEAN) default(false) info = "default value" } } } - defineValue("multiple") { + node("multiple") { info = "A sns value" multiple = true } diff --git a/dataforge-vis-spatial-gdml/src/commonMain/kotlin/hep/dataforge/vis/spatial/gdml/visualGDML.kt b/dataforge-vis-spatial-gdml/src/commonMain/kotlin/hep/dataforge/vis/spatial/gdml/visualGDML.kt index 9f3bb093..c6f6d034 100644 --- a/dataforge-vis-spatial-gdml/src/commonMain/kotlin/hep/dataforge/vis/spatial/gdml/visualGDML.kt +++ b/dataforge-vis-spatial-gdml/src/commonMain/kotlin/hep/dataforge/vis/spatial/gdml/visualGDML.kt @@ -253,9 +253,7 @@ private fun volume( } fun GDML.toVisual(block: GDMLTransformer.() -> Unit = {}): VisualGroup3D { - val context = GDMLTransformer(this).apply(block) - return context.finalize(volume(context, world)) } diff --git a/dataforge-vis-spatial/build.gradle.kts b/dataforge-vis-spatial/build.gradle.kts index ac9f58eb..6cc31048 100644 --- a/dataforge-vis-spatial/build.gradle.kts +++ b/dataforge-vis-spatial/build.gradle.kts @@ -1,10 +1,10 @@ -import scientifik.serialization +import scientifik.* plugins { id("scientifik.mpp") } -serialization() +useSerialization() kotlin { js { @@ -30,9 +30,10 @@ kotlin { } jsMain { dependencies { -// api(project(":wrappers")) + implementation(project(":ui:bootstrap"))//to be removed later implementation(npm("three", "0.114.0")) implementation(npm("three-csg-ts", "1.0.1")) + api(npm("file-saver", "2.0.2")) } } } diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt index 71766435..6d55d40c 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Material3D.kt @@ -2,6 +2,7 @@ package hep.dataforge.vis.spatial import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.attributes import hep.dataforge.names.asName import hep.dataforge.names.plus import hep.dataforge.values.ValueType @@ -39,7 +40,8 @@ class Material3D : Scheme() { val MATERIAL_KEY = "material".asName() internal val COLOR_KEY = "color".asName() val MATERIAL_COLOR_KEY = MATERIAL_KEY + COLOR_KEY - val SPECULAR_COLOR_KEY = "specularColor".asName() + internal val SPECULAR_COLOR_KEY = "specularColor".asName() + val MATERIAL_SPECULAR_COLOR_KEY = MATERIAL_KEY + SPECULAR_COLOR_KEY internal val OPACITY_KEY = "opacity".asName() val MATERIAL_OPACITY_KEY = MATERIAL_KEY + OPACITY_KEY internal val WIREFRAME_KEY = "wireframe".asName() @@ -48,21 +50,22 @@ class Material3D : Scheme() { val descriptor by lazy { //must be lazy to avoid initialization bug NodeDescriptor { - defineValue(COLOR_KEY) { + value(COLOR_KEY) { type(ValueType.STRING, ValueType.NUMBER) default("#ffffff") widgetType = "color" } - defineValue(OPACITY_KEY) { + value(OPACITY_KEY) { type(ValueType.NUMBER) default(1.0) - config["attributes"] = Meta { + attributes { this["min"] = 0.0 this["max"] = 1.0 this["step"] = 0.1 } + widgetType = "slider" } - defineValue(WIREFRAME_KEY) { + value(WIREFRAME_KEY) { type(ValueType.BOOLEAN) default(false) } diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Proxy.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Proxy.kt index b7fb006f..6a72c3f4 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Proxy.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Proxy.kt @@ -50,14 +50,18 @@ class Proxy private constructor( override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { return if (inherit) { - properties?.get(name) - ?: mergedStyles[name] - ?: prototype.getProperty(name) - ?: parent?.getProperty(name) + sequence { + yield(properties?.get(name)) + yield(mergedStyles[name]) + yield(prototype.getProperty(name)) + yield(parent?.getProperty(name, inherit)) + }.merge() } else { - properties?.get(name) - ?: mergedStyles[name] - ?: prototype.getProperty(name, false) + sequence { + yield(properties?.get(name)) + yield(mergedStyles[name]) + yield(prototype.getProperty(name, false)) + }.merge() } } @@ -125,14 +129,18 @@ class Proxy private constructor( override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { return if (inherit) { - properties?.get(name) - ?: mergedStyles[name] - ?: prototype.getProperty(name) - ?: parent?.getProperty(name) + sequence { + yield(properties?.get(name)) + yield(mergedStyles[name]) + yield(prototype.getProperty(name)) + yield(parent?.getProperty(name, inherit)) + }.merge() } else { - properties?.get(name) - ?: mergedStyles[name] - ?: prototype.getProperty(name, false) + sequence { + yield(properties?.get(name)) + yield(mergedStyles[name]) + yield(prototype.getProperty(name, false)) + }.merge() } } diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt index 8cd0369d..5c40d4fe 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt @@ -63,18 +63,18 @@ interface VisualObject3D : VisualObject { val descriptor by lazy { NodeDescriptor { - defineValue(VISIBLE_KEY) { + value(VISIBLE_KEY) { type(ValueType.BOOLEAN) default(true) } //TODO replace by descriptor merge - defineValue(VisualObject.STYLE_KEY){ + value(VisualObject.STYLE_KEY){ type(ValueType.STRING) multiple = true } - defineItem(Material3D.MATERIAL_KEY.toString(), Material3D.descriptor) + item(Material3D.MATERIAL_KEY.toString(), Material3D.descriptor) } } } diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt index 0a49e96a..1a60cccc 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvas.kt @@ -96,7 +96,7 @@ class ThreeCanvas(element: HTMLElement, val three: ThreePlugin, val canvas: Canv val picked = pick() if (picked != null && this.picked != picked) { - this.picked?.toggleHighlight(false,HIGHLIGHT_NAME, HIGHLIGHT_MATERIAL) + this.picked?.toggleHighlight(false, HIGHLIGHT_NAME, HIGHLIGHT_MATERIAL) picked.toggleHighlight(true, HIGHLIGHT_NAME, HIGHLIGHT_MATERIAL) this.picked = picked } @@ -131,10 +131,7 @@ class ThreeCanvas(element: HTMLElement, val three: ThreePlugin, val canvas: Canv } } - private fun Object3D.isStatic(): Boolean { - return false - } - + //find first non-static parent in this object ancestry private fun Object3D?.upTrace(): Object3D? = if (this?.name?.startsWith("@") == true) parent else this private fun pick(): Object3D? { @@ -144,7 +141,8 @@ class ThreeCanvas(element: HTMLElement, val three: ThreePlugin, val canvas: Canv // calculate objects intersecting the picking ray return root?.let { root -> val intersects = raycaster.intersectObject(root, true) - val obj = intersects.map { it.`object` }.firstOrNull { !it.isStatic() } + //skip invisible objects + val obj = intersects.map { it.`object` }.firstOrNull { it.visible } obj.upTrace() } } @@ -168,7 +166,7 @@ class ThreeCanvas(element: HTMLElement, val three: ThreePlugin, val canvas: Canv } } - fun clear(){ + fun clear() { scene.children.find { it.name == "@root" }?.let { scene.remove(it) } @@ -186,7 +184,7 @@ class ThreeCanvas(element: HTMLElement, val three: ThreePlugin, val canvas: Canv root = object3D } - private var highlighted: Object3D? = null + private var selected: Object3D? = null /** * Toggle highlight for the given [Mesh] object @@ -224,15 +222,15 @@ class ThreeCanvas(element: HTMLElement, val three: ThreePlugin, val canvas: Canv */ fun select(name: Name?) { if (name == null) { - highlighted?.toggleHighlight(false, SELECT_NAME, SELECTED_MATERIAL) - highlighted = null + selected?.toggleHighlight(false, SELECT_NAME, SELECTED_MATERIAL) + selected = null return } val obj = root?.findChild(name) - if (obj != null && highlighted != obj) { - highlighted?.toggleHighlight(false, SELECT_NAME, SELECTED_MATERIAL) + if (obj != null && selected != obj) { + selected?.toggleHighlight(false, SELECT_NAME, SELECTED_MATERIAL) obj.toggleHighlight(true, SELECT_NAME, SELECTED_MATERIAL) - highlighted = obj + selected = obj } } diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt index 37fbc997..a5618284 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt @@ -29,7 +29,7 @@ interface ThreeCanvasState : RState { class ThreeCanvasComponent : RComponent() { - var canvas: ThreeCanvas? = null + private var canvas: ThreeCanvas? = null override fun componentDidMount() { if(canvas == null) { diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeMaterials.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeMaterials.kt index be99f14f..342212fe 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeMaterials.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeMaterials.kt @@ -5,6 +5,7 @@ import hep.dataforge.values.ValueType import hep.dataforge.vis.Colors import hep.dataforge.vis.VisualObject import hep.dataforge.vis.spatial.Material3D +import hep.dataforge.vis.spatial.Material3D.Companion.MATERIAL_SPECULAR_COLOR_KEY import info.laht.threekt.materials.LineBasicMaterial import info.laht.threekt.materials.Material import info.laht.threekt.materials.MeshBasicMaterial diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/outputConfig.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/outputConfig.kt index a2796502..196651b9 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/outputConfig.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/outputConfig.kt @@ -1,8 +1,8 @@ package hep.dataforge.vis.spatial.three -import hep.dataforge.js.accordion -import hep.dataforge.js.entry import hep.dataforge.js.requireJS +import hep.dataforge.vis.bootstrap.accordion +import hep.dataforge.vis.bootstrap.entry import hep.dataforge.vis.spatial.Visual3D import hep.dataforge.vis.spatial.VisualGroup3D import kotlinx.html.* diff --git a/demo/gdml/build.gradle.kts b/demo/gdml/build.gradle.kts index 798a866d..fdaeadc8 100644 --- a/demo/gdml/build.gradle.kts +++ b/demo/gdml/build.gradle.kts @@ -1,6 +1,4 @@ -import scientifik.DependencyConfiguration -import scientifik.FXModule -import scientifik.fx +import scientifik.* plugins { id("scientifik.mpp") @@ -8,7 +6,7 @@ plugins { } val fxVersion: String by rootProject.extra -fx(FXModule.CONTROLS, version = fxVersion, configuration = DependencyConfiguration.IMPLEMENTATION) +useFx(FXModule.CONTROLS, version = fxVersion, configuration = DependencyConfiguration.IMPLEMENTATION) kotlin { @@ -17,6 +15,7 @@ kotlin { } js { + useCommonJs() browser { webpackTask { //sourceMaps = false @@ -27,8 +26,14 @@ kotlin { sourceSets { commonMain { dependencies { - api(project(":dataforge-vis-spatial")) - api(project(":dataforge-vis-spatial-gdml")) + implementation(project(":dataforge-vis-spatial")) + implementation(project(":dataforge-vis-spatial-gdml")) + } + } + jsMain{ + dependencies { + implementation(project(":ui:bootstrap")) + implementation(npm("react-file-drop", "3.0.6")) } } } diff --git a/demo/gdml/src/jsMain/kotlin/drop/FileDrop.kt b/demo/gdml/src/jsMain/kotlin/drop/FileDrop.kt new file mode 100644 index 00000000..2799eead --- /dev/null +++ b/demo/gdml/src/jsMain/kotlin/drop/FileDrop.kt @@ -0,0 +1,40 @@ +@file:JsModule("react-file-drop") +@file:JsNonModule + +package drop + +import org.w3c.dom.DragEvent +import org.w3c.files.FileList +import react.* + +external enum class DropEffects { + copy, + move, + link, + none +} + +external interface FileDropProps: RProps { + var className: String? + var targetClassName: String? + var draggingOverFrameClassName: String? + var draggingOverTargetClassName: String? + +// var frame?: Exclude | HTMLDocument; + var onFrameDragEnter: ((event: DragEvent) -> Unit)? + var onFrameDragLeave: ((event: DragEvent) -> Unit)? + var onFrameDrop: ((event: DragEvent) -> Unit)? +// var onDragOver: ReactDragEventHandler? +// var onDragLeave: ReactDragEventHandler? + var onDrop: ((files: FileList?, event: dynamic) -> Unit)?//event:DragEvent) + var dropEffect: DropEffects? +} + +external interface FileDropState: RState { + var draggingOverFrame: Boolean + var draggingOverTarget: Boolean +} + +external class FileDrop : Component { + override fun render(): dynamic +} \ No newline at end of file diff --git a/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLAppComponent.kt b/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLAppComponent.kt new file mode 100644 index 00000000..6f3cae91 --- /dev/null +++ b/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLAppComponent.kt @@ -0,0 +1,138 @@ +package hep.dataforge.vis.spatial.gdml.demo + +import hep.dataforge.context.Context +import hep.dataforge.names.Name +import hep.dataforge.names.isEmpty +import hep.dataforge.vis.VisualGroup +import hep.dataforge.vis.VisualObject +import hep.dataforge.vis.bootstrap.* +import hep.dataforge.vis.react.component +import hep.dataforge.vis.react.state +import hep.dataforge.vis.spatial.VisualGroup3D +import hep.dataforge.vis.spatial.VisualObject3D +import hep.dataforge.vis.spatial.gdml.toVisual +import hep.dataforge.vis.spatial.specifications.Camera +import hep.dataforge.vis.spatial.specifications.Canvas +import hep.dataforge.vis.spatial.three.ThreeCanvas +import hep.dataforge.vis.spatial.three.ThreeCanvasComponent +import hep.dataforge.vis.spatial.three.canvasControls +import org.w3c.files.FileReader +import org.w3c.files.get +import react.RProps +import react.dom.h1 +import scientifik.gdml.GDML +import scientifik.gdml.parse +import kotlin.browser.window +import kotlin.math.PI + +interface GDMLAppProps : RProps { + var context: Context + var rootObject: VisualObject? + var selected: Name? +} + +private val canvasConfig = Canvas { + camera = Camera { + distance = 2100.0 + latitude = PI / 6 + azimuth = PI + PI / 6 + } +} + +val GDMLApp = component { props -> + var selected by state { props.selected } + var canvas: ThreeCanvas? by state { null } + var visual: VisualObject? by state { props.rootObject } + + val select: (Name?) -> Unit = { + selected = it + } + + fun loadData(name: String, data: String) { + visual = when { + name.endsWith(".gdml") || name.endsWith(".xml") -> { + val gdml = GDML.parse(data) + gdml.toVisual(gdmlConfiguration) + } + name.endsWith(".json") -> VisualGroup3D.parseJson(data) + else -> { + window.alert("File extension is not recognized: $name") + error("File extension is not recognized: $name") + } + } + } + + flexColumn { + h1 { +"GDML/JSON loader demo" } + gridRow { + gridColumn(3) { + card("Load data") { + fileDrop("(drag file here)") { files -> + val file = files?.get(0) + if (file != null) { + + FileReader().apply { + onload = { + val string = result as String + loadData(file.name, string) + } + readAsText(file) + } + } + } + } + //tree + card("Object tree") { + visual?.let { + objectTree(it, selected, select) + } + } + } + + gridColumn(6) { + //canvas + (visual as? VisualObject3D)?.let { visual3D -> + child(ThreeCanvasComponent::class) { + attrs { + this.context = props.context + this.obj = visual3D + this.selected = selected + this.clickCallback = select + this.canvasCallback = { + canvas = it + } + } + } + } + } + gridColumn(3) { + gridRow { + //settings + canvas?.let { + card("Canvas configuration") { + canvasControls(it) + } + } + } + gridRow { + namecrumbs(selected, "World") { selected = it } + } + gridRow { + //properties + card("Properties") { + selected.let { selected -> + val selectedObject: VisualObject? = when { + selected == null -> null + selected.isEmpty() -> visual + else -> (visual as? VisualGroup)?.get(selected) + } + if (selectedObject != null) { + configEditor(selectedObject, default = selectedObject.allProperties(), key = selected) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt b/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt index 0c451902..3c969d00 100644 --- a/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt +++ b/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt @@ -3,193 +3,72 @@ package hep.dataforge.vis.spatial.gdml.demo import hep.dataforge.context.Global import hep.dataforge.js.Application import hep.dataforge.js.startApplication -import hep.dataforge.names.Name -import hep.dataforge.names.isEmpty -import hep.dataforge.vis.VisualGroup -import hep.dataforge.vis.VisualObject -import hep.dataforge.vis.editor.renderObjectTree -import hep.dataforge.vis.editor.visualPropertyEditor import hep.dataforge.vis.spatial.Material3D.Companion.MATERIAL_OPACITY_KEY -import hep.dataforge.vis.spatial.VisualGroup3D -import hep.dataforge.vis.spatial.VisualObject3D import hep.dataforge.vis.spatial.gdml.GDMLTransformer import hep.dataforge.vis.spatial.gdml.LUnit import hep.dataforge.vis.spatial.gdml.toVisual -import hep.dataforge.vis.spatial.three.ThreePlugin -import hep.dataforge.vis.spatial.three.displayCanvasControls -import hep.dataforge.vis.spatial.three.output -import org.w3c.dom.* -import org.w3c.files.FileList -import org.w3c.files.FileReader -import org.w3c.files.get -import scientifik.gdml.GDML +import react.child +import react.dom.div +import react.dom.render import kotlin.browser.document -import kotlin.browser.window -import kotlin.dom.clear + + +val gdmlConfiguration: GDMLTransformer.() -> Unit = { + lUnit = LUnit.CM + volumeAction = { volume -> + when { + volume.name.startsWith("ecal01lay") -> GDMLTransformer.Action.REJECT + volume.name.startsWith("UPBL") -> GDMLTransformer.Action.REJECT + volume.name.startsWith("USCL") -> GDMLTransformer.Action.REJECT + volume.name.startsWith("VPBL") -> GDMLTransformer.Action.REJECT + volume.name.startsWith("VSCL") -> GDMLTransformer.Action.REJECT + else -> GDMLTransformer.Action.CACHE + } + } + + solidConfiguration = { parent, solid -> + if ( + solid.name.startsWith("Yoke") + || solid.name.startsWith("Pole") + || parent.physVolumes.isNotEmpty() + ) { + useStyle("opaque") { + MATERIAL_OPACITY_KEY put 0.3 + } + } + } +} private class GDMLDemoApp : Application { - /** - * Handle mouse drag according to https://www.html5rocks.com/en/tutorials/file/dndfiles/ - */ - private fun handleDragOver(event: DragEvent) { - event.stopPropagation() - event.preventDefault() - event.dataTransfer?.dropEffect = "copy" - } - - /** - * Load data from text file - */ - private fun loadData(event: DragEvent, block: (name: String, data: String) -> Unit) { - event.stopPropagation() - event.preventDefault() - - val file = (event.dataTransfer?.files as FileList)[0] - ?: throw RuntimeException("Failed to load file") - - FileReader().apply { - onload = { - val string = result as String - block(file.name, string) - } - readAsText(file) - } - } - - private fun spinner(show: Boolean) { -// if( show){ -// -// val style = if (show) { -// "display:block;" -// } else { -// "display:none;" -// } -// document.getElementById("canvas")?.append { -// -// } - } - - private fun message(message: String?) { - console.log(message) -// document.getElementById("messages")?.let { element -> -// if (message == null) { -// element.clear() -// } else { -// element.append { -// p { -// +message -// } -// } -// } -// } - } - - private val gdmlConfiguration: GDMLTransformer.() -> Unit = { - lUnit = LUnit.CM - volumeAction = { volume -> - when { - volume.name.startsWith("ecal01lay") -> GDMLTransformer.Action.REJECT - volume.name.startsWith("UPBL") -> GDMLTransformer.Action.REJECT - volume.name.startsWith("USCL") -> GDMLTransformer.Action.REJECT - volume.name.startsWith("VPBL") -> GDMLTransformer.Action.REJECT - volume.name.startsWith("VSCL") -> GDMLTransformer.Action.REJECT - else -> GDMLTransformer.Action.CACHE - } - } - - solidConfiguration = { parent, solid -> - if ( - solid.name.startsWith("Yoke") - || solid.name.startsWith("Pole") - || parent.physVolumes.isNotEmpty() - ) { - useStyle("opaque") { - MATERIAL_OPACITY_KEY put 0.3 - } - } - } - } - override fun start(state: Map) { val context = Global.context("demo") {} - val three = context.plugins.load(ThreePlugin) - //val url = URL("https://drive.google.com/open?id=1w5e7fILMN83JGgB8WANJUYm8OW2s0WVO") + val element = document.getElementById("app") ?: error("Element with id 'app' not found on page") - val canvasElement = document.getElementById("canvas") ?: error("Element with id 'canvas' not found on page") - val configElement = document.getElementById("config") ?: error("Element with id 'layers' not found on page") - val treeElement = document.getElementById("tree") ?: error("Element with id 'tree' not found on page") - val editorElement = document.getElementById("editor") ?: error("Element with id 'editor' not found on page") - canvasElement.clear() - - val action: (name: String, data: String) -> Unit = { name, data -> - canvasElement.clear() - spinner(true) - val visual: VisualObject3D = when { - name.endsWith(".gdml") || name.endsWith(".xml") -> { - message("Loading GDML") - val gdml = GDML.format.parse(GDML.serializer(), data) - message("Converting GDML into DF-VIS format") - gdml.toVisual(gdmlConfiguration) - } - name.endsWith(".json") -> VisualGroup3D.parseJson(data) - else -> { - window.alert("File extension is not recognized: $name") - error("File extension is not recognized: $name") - } - } - - //Optimize tree - //(visual as? VisualGroup3D)?.transformInPlace(UnRef, RemoveSingleChild) - - message("Rendering") - - //output.camera.layers.enable(1) - val canvas = three.output(canvasElement as HTMLElement) - - canvas.camera.layers.set(0) - configElement.displayCanvasControls(canvas) - //tree.visualObjectTree(visual, editor::propertyEditor) - fun selectElement(name: Name) { - val child: VisualObject = when { - name.isEmpty() -> visual - visual is VisualGroup -> visual[name] ?: return - else -> return - } - canvas.select(name) - editorElement.visualPropertyEditor(name, child) - - } - -// canvas.clickListener = ::selectElement - - //tree.visualObjectTree(visual, editor::propertyEditor) - treeElement.renderObjectTree(visual) { treeName -> - selectElement(treeName) - } - canvas.render(visual) - message(null) - spinner(false) - } - - (document.getElementById("drop_zone") as? HTMLDivElement)?.apply { - addEventListener("dragover", { handleDragOver(it as DragEvent) }, false) - addEventListener("drop", { loadData(it as DragEvent, action) }, false) - } - (document.getElementById("file_load_button") as? HTMLInputElement)?.apply { - addEventListener("change", { - (it.target as HTMLInputElement).files?.asList()?.first()?.let { file -> - FileReader().apply { - onload = { - val string = result as String - action(file.name, string) - } - readAsText(file) + render(element) { + div("h-100") { + child(GDMLApp) { + attrs { + this.context = context + this.rootObject = cubes().toVisual(gdmlConfiguration) } } - }, false) + } } +// (document.getElementById("file_load_button") as? HTMLInputElement)?.apply { +// addEventListener("change", { +// (it.target as HTMLInputElement).files?.asList()?.first()?.let { file -> +// FileReader().apply { +// onload = { +// val string = result as String +// action(file.name, string) +// } +// readAsText(file) +// } +// } +// }, false) +// } } } diff --git a/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/fileDrop.kt b/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/fileDrop.kt new file mode 100644 index 00000000..aa0445aa --- /dev/null +++ b/demo/gdml/src/jsMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/fileDrop.kt @@ -0,0 +1,30 @@ +package hep.dataforge.vis.spatial.gdml.demo + +import drop.FileDrop +import kotlinx.css.* +import kotlinx.css.properties.border +import org.w3c.files.FileList +import react.RBuilder +import styled.css +import styled.styledDiv + +//TODO move styles to inline + +fun RBuilder.fileDrop(title: String, action: (files: FileList?) -> Unit) { + styledDiv { + css { + border(style = BorderStyle.dashed, width = 1.px, color = Color.orange) + alignContent = Align.center + } + + child(FileDrop::class) { + attrs { + onDrop = { files, _ -> + console.info("loaded $files") + action(files) + } + } + +title + } + } +} \ No newline at end of file diff --git a/demo/gdml/src/jsMain/resources/css/fileDrop.css b/demo/gdml/src/jsMain/resources/css/fileDrop.css new file mode 100644 index 00000000..6f66e353 --- /dev/null +++ b/demo/gdml/src/jsMain/resources/css/fileDrop.css @@ -0,0 +1,62 @@ +.file-drop { + /* relatively position the container bc the contents are absolute */ + position: relative; + height: 100px; + width: 100%; +} + +.file-drop > .file-drop-target { + /* basic styles */ + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + border-radius: 2px; + + /* horizontally and vertically center all content */ + display: flex; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + + flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + + align-items: center; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + + justify-content: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + + align-content: center; + -webkit-align-content: center; + -ms-flex-line-pack: center; + + text-align: center; +} + +.file-drop > .file-drop-target.file-drop-dragging-over-frame { + /* overlay a black mask when dragging over the frame */ + border: none; + background-color: rgba(0, 0, 0, 0.65); + box-shadow: none; + z-index: 50; + opacity: 1; + + /* typography */ + color: white; +} + +.file-drop > .file-drop-target.file-drop-dragging-over-target { + /* turn stuff orange when we are dragging over the target */ + color: #ff6e40; + box-shadow: 0 0 13px 3px #ff6e40; +} \ No newline at end of file diff --git a/demo/gdml/src/jsMain/resources/css/jsoneditor.min.css b/demo/gdml/src/jsMain/resources/css/jsoneditor.min.css deleted file mode 100644 index 63190af9..00000000 --- a/demo/gdml/src/jsMain/resources/css/jsoneditor.min.css +++ /dev/null @@ -1,6 +0,0 @@ -.jsoneditor .search input{height:auto;border:inherit;border:none;box-shadow:none}.jsoneditor table{border-collapse:collapse;width:auto}.jsoneditor td,.jsoneditor th{padding:0;display:table-cell;text-align:left;vertical-align:inherit;border-radius:inherit}.jsoneditor{color:#1a1a1a;border:thin solid #3883fa;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;height:100%;position:relative;padding:0;line-height:100%}div.jsoneditor-default,div.jsoneditor-field,div.jsoneditor-readonly,div.jsoneditor-value{border:1px solid transparent;min-height:16px;min-width:32px;padding:2px;margin:1px;word-wrap:break-word;float:left}div.jsoneditor-field p,div.jsoneditor-value p{margin:0}div.jsoneditor-value{word-break:break-word}div.jsoneditor-value.jsoneditor-empty::after{content:"value"}div.jsoneditor-value.jsoneditor-string{color:#006000}div.jsoneditor-value.jsoneditor-number{color:#ee422e}div.jsoneditor-value.jsoneditor-boolean{color:#ff8c00}div.jsoneditor-value.jsoneditor-null{color:#004ed0}div.jsoneditor-value.jsoneditor-invalid{color:#000}div.jsoneditor-readonly{min-width:16px;color:grey}div.jsoneditor-empty{border-color:#d3d3d3;border-style:dashed;border-radius:2px}div.jsoneditor-field.jsoneditor-empty::after{content:"field"}div.jsoneditor td{vertical-align:top}div.jsoneditor td.jsoneditor-separator{padding:3px 0;vertical-align:top;color:grey}div.jsoneditor td.jsoneditor-tree{vertical-align:top}div.jsoneditor div.jsoneditor-anchor{cursor:pointer}div.jsoneditor div.jsoneditor-anchor .picker_wrapper.popup.popup_bottom{top:28px;left:-10px}div.jsoneditor.busy pre.jsoneditor-preview{background:#f5f5f5;color:grey}div.jsoneditor.busy div.jsoneditor-busy{display:inherit}div.jsoneditor code.jsoneditor-preview{background:0 0}div.jsoneditor.jsoneditor-mode-preview pre.jsoneditor-preview{width:100%;height:100%;box-sizing:border-box;overflow:auto;padding:2px;margin:0;white-space:pre-wrap;word-break:break-all}div.jsoneditor-default{color:grey;padding-left:10px}div.jsoneditor-tree{width:100%;height:100%;position:relative;overflow:auto}div.jsoneditor-tree button.jsoneditor-button{width:24px;height:24px;padding:0;margin:0;border:none;cursor:pointer;background:transparent url(../img/jsoneditor-icons.svg)}div.jsoneditor-tree button.jsoneditor-button:focus{background-color:#f5f5f5;outline:#e5e5e5 solid 1px}div.jsoneditor-tree button.jsoneditor-collapsed{background-position:0 -48px}div.jsoneditor-tree button.jsoneditor-expanded{background-position:0 -72px}div.jsoneditor-tree button.jsoneditor-contextmenu{background-position:-48px -72px}div.jsoneditor-tree button.jsoneditor-invisible{visibility:hidden;background:0 0}div.jsoneditor-tree button.jsoneditor-dragarea{background:url(../img/jsoneditor-icons.svg) -72px -72px;cursor:move}div.jsoneditor-tree :focus{outline:0}div.jsoneditor-tree div.jsoneditor-show-more{display:inline-block;padding:3px 4px;margin:2px 0;background-color:#e5e5e5;border-radius:3px;color:grey;font-family:arial,sans-serif;font-size:10pt}div.jsoneditor-tree div.jsoneditor-show-more a{display:inline-block;color:grey}div.jsoneditor-tree div.jsoneditor-color{display:inline-block;width:12px;height:12px;margin:4px;border:1px solid grey;cursor:pointer}div.jsoneditor-tree div.jsoneditor-date{background:#a1a1a1;color:#fff;font-family:arial,sans-serif;border-radius:3px;display:inline-block;padding:3px;margin:0 3px}div.jsoneditor-tree table.jsoneditor-tree{border-collapse:collapse;border-spacing:0;width:100%}div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error{width:24px;height:24px;padding:0;margin:0 4px 0 0;background:url(../img/jsoneditor-icons.svg) -168px -48px}div.jsoneditor-outer{position:static;width:100%;height:100%;margin:0;padding:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}div.jsoneditor-outer.has-nav-bar{margin-top:-26px;padding-top:26px}div.jsoneditor-outer.has-nav-bar.has-main-menu-bar{margin-top:-61px;padding-top:61px}div.jsoneditor-outer.has-status-bar{margin-bottom:-26px;padding-bottom:26px}div.jsoneditor-outer.has-main-menu-bar{margin-top:-35px;padding-top:35px}div.jsoneditor-busy{position:absolute;top:15%;left:0;box-sizing:border-box;width:100%;text-align:center;display:none}div.jsoneditor-busy span{background-color:#ffffab;border:1px solid #fe0;border-radius:3px;padding:5px 15px;box-shadow:0 0 5px rgba(0,0,0,.4)}div.jsoneditor-field.jsoneditor-empty::after,div.jsoneditor-value.jsoneditor-empty::after{pointer-events:none;color:#d3d3d3;font-size:8pt}a.jsoneditor-value.jsoneditor-url,div.jsoneditor-value.jsoneditor-url{color:#006000;text-decoration:underline}a.jsoneditor-value.jsoneditor-url{display:inline-block;padding:2px;margin:2px}a.jsoneditor-value.jsoneditor-url:focus,a.jsoneditor-value.jsoneditor-url:hover{color:#ee422e}div.jsoneditor-field.jsoneditor-highlight,div.jsoneditor-field[contenteditable=true]:focus,div.jsoneditor-field[contenteditable=true]:hover,div.jsoneditor-value.jsoneditor-highlight,div.jsoneditor-value[contenteditable=true]:focus,div.jsoneditor-value[contenteditable=true]:hover{background-color:#ffffab;border:1px solid #fe0;border-radius:2px}div.jsoneditor-field.jsoneditor-highlight-active,div.jsoneditor-field.jsoneditor-highlight-active:focus,div.jsoneditor-field.jsoneditor-highlight-active:hover,div.jsoneditor-value.jsoneditor-highlight-active,div.jsoneditor-value.jsoneditor-highlight-active:focus,div.jsoneditor-value.jsoneditor-highlight-active:hover{background-color:#fe0;border:1px solid #ffc700;border-radius:2px}div.jsoneditor-value.jsoneditor-array,div.jsoneditor-value.jsoneditor-object{min-width:16px}div.jsoneditor-mode-form tr.jsoneditor-expandable td.jsoneditor-tree,div.jsoneditor-mode-view tr.jsoneditor-expandable td.jsoneditor-tree{cursor:pointer}div.jsoneditor-tree button.jsoneditor-contextmenu.jsoneditor-selected,div.jsoneditor-tree button.jsoneditor-contextmenu:focus,div.jsoneditor-tree button.jsoneditor-contextmenu:hover,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu{background-position:-48px -48px}div.jsoneditor-tree div.jsoneditor-show-more a:focus,div.jsoneditor-tree div.jsoneditor-show-more a:hover{color:#ee422e}.ace-jsoneditor,textarea.jsoneditor-text{min-height:150px}textarea.jsoneditor-text{width:100%;height:100%;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;outline-width:0;border:none;background-color:#fff;resize:none}tr.jsoneditor-highlight,tr.jsoneditor-selected{background-color:#d3d3d3}tr.jsoneditor-selected button.jsoneditor-contextmenu,tr.jsoneditor-selected button.jsoneditor-dragarea{visibility:hidden}tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea{visibility:visible}div.jsoneditor-tree button.jsoneditor-dragarea:focus,div.jsoneditor-tree button.jsoneditor-dragarea:hover,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea{background-position:-72px -48px}div.jsoneditor td,div.jsoneditor th,div.jsoneditor tr{padding:0;margin:0}.jsoneditor-popover,.jsoneditor-schema-error,div.jsoneditor td,div.jsoneditor textarea,div.jsoneditor th,div.jsoneditor-field,div.jsoneditor-value,pre.jsoneditor-preview{font-family:"dejavu sans mono","droid sans mono",consolas,monaco,"lucida console","courier new",courier,monospace,sans-serif;font-size:10pt;color:#1a1a1a}.jsoneditor-schema-error{cursor:default;display:inline-block;height:24px;line-height:24px;position:relative;text-align:center;width:24px}.jsoneditor-popover{background-color:#4c4c4c;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.4);color:#fff;padding:7px 10px;position:absolute;cursor:auto;width:200px;z-index:999}.jsoneditor-popover.jsoneditor-above{bottom:32px;left:-98px}.jsoneditor-popover.jsoneditor-above:before{border-top:7px solid #4c4c4c;bottom:-7px}.jsoneditor-popover.jsoneditor-below{top:32px;left:-98px}.jsoneditor-popover.jsoneditor-below:before{border-bottom:7px solid #4c4c4c;top:-7px}.jsoneditor-popover.jsoneditor-left{top:-7px;right:32px}.jsoneditor-popover.jsoneditor-left:before{border-left:7px solid #4c4c4c;border-top:7px solid transparent;border-bottom:7px solid transparent;content:"";top:19px;right:-14px;left:inherit;margin-left:inherit;margin-top:-7px;position:absolute}.jsoneditor-popover.jsoneditor-right{top:-7px;left:32px}.jsoneditor-popover.jsoneditor-right:before{border-right:7px solid #4c4c4c;border-top:7px solid transparent;border-bottom:7px solid transparent;content:"";top:19px;left:-14px;margin-left:inherit;margin-top:-7px;position:absolute}.jsoneditor-popover:before{border-right:7px solid transparent;border-left:7px solid transparent;content:"";display:block;left:50%;margin-left:-7px;position:absolute}.jsoneditor-text-errors tr.jump-to-line:hover{text-decoration:underline;cursor:pointer}.jsoneditor-schema-error:focus .jsoneditor-popover,.jsoneditor-schema-error:hover .jsoneditor-popover{display:block;animation:fade-in .3s linear 1,move-up .3s linear 1}@keyframes fade-in{from{opacity:0}to{opacity:1}}.jsoneditor .jsoneditor-validation-errors-container{max-height:130px;overflow-y:auto}.jsoneditor .jsoneditor-validation-errors{width:100%;overflow:hidden}.jsoneditor .jsoneditor-additional-errors{position:absolute;margin:auto;bottom:31px;left:calc(50% - 92px);color:grey;background-color:#ebebeb;padding:7px 15px;border-radius:8px}.jsoneditor .jsoneditor-additional-errors.visible{visibility:visible;opacity:1;transition:opacity 2s linear}.jsoneditor .jsoneditor-additional-errors.hidden{visibility:hidden;opacity:0;transition:visibility 0s 2s,opacity 2s linear}.jsoneditor .jsoneditor-text-errors{width:100%;border-collapse:collapse;border-top:1px solid #ffc700}.jsoneditor .jsoneditor-text-errors td{padding:3px 6px;vertical-align:middle}.jsoneditor .jsoneditor-text-errors td pre{margin:0;white-space:normal}.jsoneditor .jsoneditor-text-errors tr{background-color:#ffffab}.jsoneditor .jsoneditor-text-errors tr.parse-error{background-color:#ee2e2e70}.jsoneditor-text-errors .jsoneditor-schema-error{border:none;width:24px;height:24px;padding:0;margin:0 4px 0 0;cursor:pointer}.jsoneditor-text-errors tr .jsoneditor-schema-error{background:url(../img/jsoneditor-icons.svg) -168px -48px}.jsoneditor-text-errors tr.parse-error .jsoneditor-schema-error{background:url(../img/jsoneditor-icons.svg) -25px 0}.fadein{-webkit-animation:fadein .3s;animation:fadein .3s;-moz-animation:fadein .3s;-o-animation:fadein .3s}@keyframes fadein{0%{opacity:0}100%{opacity:1}}.jsoneditor-contextmenu-root{position:relative;width:0;height:0}.jsoneditor-contextmenu{position:absolute;box-sizing:content-box;z-index:99}.jsoneditor-contextmenu .jsoneditor-menu{position:relative;left:0;top:0;width:128px;height:auto;background:#fff;border:1px solid #d3d3d3;box-shadow:2px 2px 12px rgba(128,128,128,.3);list-style:none;margin:0;padding:0}.jsoneditor-contextmenu .jsoneditor-menu button{position:relative;padding:0 4px 0 0;margin:0;width:128px;height:auto;border:none;cursor:pointer;color:#4d4d4d;background:0 0;font-size:10pt;font-family:arial,sans-serif;box-sizing:border-box;text-align:left}.jsoneditor-contextmenu .jsoneditor-menu button::-moz-focus-inner{padding:0;border:0}.jsoneditor-contextmenu .jsoneditor-menu button.jsoneditor-default{width:96px}.jsoneditor-contextmenu .jsoneditor-menu button.jsoneditor-expand{float:right;width:32px;height:24px;border-left:1px solid #e5e5e5}.jsoneditor-contextmenu .jsoneditor-menu li{overflow:hidden}.jsoneditor-contextmenu .jsoneditor-menu li ul{display:none;position:relative;left:-10px;top:0;border:none;box-shadow:inset 0 0 10px rgba(128,128,128,.5);padding:0 10px;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.jsoneditor-contextmenu .jsoneditor-menu li ul .jsoneditor-icon{margin-left:24px}.jsoneditor-contextmenu .jsoneditor-menu li ul li button{padding-left:24px;animation:all ease-in-out 1s}.jsoneditor-contextmenu .jsoneditor-menu li button .jsoneditor-expand{position:absolute;top:0;right:0;width:24px;height:24px;padding:0;margin:0 4px 0 0;background:url(../img/jsoneditor-icons.svg) 0 -72px}.jsoneditor-contextmenu .jsoneditor-icon{position:absolute;top:0;left:0;width:24px;height:24px;border:none;padding:0;margin:0;background-image:url(../img/jsoneditor-icons.svg)}.jsoneditor-contextmenu .jsoneditor-text{padding:4px 0 4px 24px;word-wrap:break-word}.jsoneditor-contextmenu .jsoneditor-text.jsoneditor-right-margin{padding-right:24px}.jsoneditor-contextmenu .jsoneditor-separator{height:0;border-top:1px solid #e5e5e5;padding-top:5px;margin-top:5px}.jsoneditor-contextmenu button.jsoneditor-remove .jsoneditor-icon{background-position:-24px 0}.jsoneditor-contextmenu button.jsoneditor-append .jsoneditor-icon{background-position:0 0}.jsoneditor-contextmenu button.jsoneditor-insert .jsoneditor-icon{background-position:0 0}.jsoneditor-contextmenu button.jsoneditor-duplicate .jsoneditor-icon{background-position:-48px 0}.jsoneditor-contextmenu button.jsoneditor-sort-asc .jsoneditor-icon{background-position:-168px 0}.jsoneditor-contextmenu button.jsoneditor-sort-desc .jsoneditor-icon{background-position:-192px 0}.jsoneditor-contextmenu button.jsoneditor-transform .jsoneditor-icon{background-position:-216px 0}.jsoneditor-contextmenu button.jsoneditor-extract .jsoneditor-icon{background-position:0 -24px}.jsoneditor-contextmenu button.jsoneditor-type-string .jsoneditor-icon{background-position:-144px 0}.jsoneditor-contextmenu button.jsoneditor-type-auto .jsoneditor-icon{background-position:-120px 0}.jsoneditor-contextmenu button.jsoneditor-type-object .jsoneditor-icon{background-position:-72px 0}.jsoneditor-contextmenu button.jsoneditor-type-array .jsoneditor-icon{background-position:-96px 0}.jsoneditor-contextmenu button.jsoneditor-type-modes .jsoneditor-icon{background-image:none;width:6px}.jsoneditor-contextmenu li,.jsoneditor-contextmenu ul{box-sizing:content-box;position:relative}.jsoneditor-contextmenu .jsoneditor-menu button:focus,.jsoneditor-contextmenu .jsoneditor-menu button:hover{color:#1a1a1a;background-color:#f5f5f5;outline:0}.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected,.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:focus,.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:hover{color:#fff;background-color:#ee422e}.jsoneditor-contextmenu .jsoneditor-menu li ul li button:focus,.jsoneditor-contextmenu .jsoneditor-menu li ul li button:hover{background-color:#f5f5f5}.jsoneditor-modal{max-width:95%;border-radius:2px!important;padding:45px 15px 15px 15px!important;box-shadow:2px 2px 12px rgba(128,128,128,.3);color:#4d4d4d;line-height:1.3em}.jsoneditor-modal.jsoneditor-modal-transform{width:600px!important}.jsoneditor-modal .pico-modal-header{position:absolute;box-sizing:border-box;top:0;left:0;width:100%;padding:0 10px;height:30px;line-height:30px;font-family:arial,sans-serif;font-size:11pt;background:#3883fa;color:#fff}.jsoneditor-modal table{width:100%}.jsoneditor-modal table td{padding:3px 0}.jsoneditor-modal table td.jsoneditor-modal-input{text-align:right;padding-right:0;white-space:nowrap}.jsoneditor-modal table td.jsoneditor-modal-actions{padding-top:15px}.jsoneditor-modal table th{vertical-align:middle}.jsoneditor-modal p:first-child{margin-top:0}.jsoneditor-modal a{color:#3883fa}.jsoneditor-modal .jsoneditor-jmespath-block{margin-bottom:10px}.jsoneditor-modal .pico-close{background:0 0!important;font-size:24px!important;top:7px!important;right:7px!important;color:#fff}.jsoneditor-modal input{padding:4px}.jsoneditor-modal input[type=text]{cursor:inherit}.jsoneditor-modal input[disabled]{background:#d3d3d3;color:grey}.jsoneditor-modal .jsoneditor-select-wrapper{position:relative;display:inline-block}.jsoneditor-modal .jsoneditor-select-wrapper:after{content:"";width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:6px solid #666;position:absolute;right:8px;top:14px;pointer-events:none}.jsoneditor-modal select{padding:3px 24px 3px 10px;min-width:180px;max-width:350px;-webkit-appearance:none;-moz-appearance:none;appearance:none;text-indent:0;text-overflow:"";font-size:10pt;line-height:1.5em}.jsoneditor-modal select::-ms-expand{display:none}.jsoneditor-modal .jsoneditor-button-group input{padding:4px 10px;margin:0;border-radius:0;border-left-style:none}.jsoneditor-modal .jsoneditor-button-group input.jsoneditor-button-first{border-top-left-radius:3px;border-bottom-left-radius:3px;border-left-style:solid}.jsoneditor-modal .jsoneditor-button-group input.jsoneditor-button-last{border-top-right-radius:3px;border-bottom-right-radius:3px}.jsoneditor-modal .jsoneditor-transform-preview{background:#f5f5f5;height:200px}.jsoneditor-modal .jsoneditor-transform-preview.jsoneditor-error{color:#ee422e}.jsoneditor-modal .jsoneditor-jmespath-wizard{line-height:1.2em;width:100%;padding:0;border-radius:3px}.jsoneditor-modal .jsoneditor-jmespath-label{font-weight:700;color:#1e90ff;margin-top:20px;margin-bottom:5px}.jsoneditor-modal .jsoneditor-jmespath-wizard-table{width:100%}.jsoneditor-modal .jsoneditor-jmespath-wizard-label{font-style:italic;margin:4px 0 2px 0}.jsoneditor-modal .jsoneditor-inline{position:relative;display:inline-block;width:100%;padding-top:2px;padding-bottom:2px}.jsoneditor-modal .jsoneditor-inline:not(:last-child){padding-right:2px}.jsoneditor-modal .jsoneditor-jmespath-filter{display:flex;flex-wrap:wrap}.jsoneditor-modal .jsoneditor-jmespath-filter-field{width:180px}.jsoneditor-modal .jsoneditor-jmespath-filter-relation{width:100px}.jsoneditor-modal .jsoneditor-jmespath-filter-value{min-width:180px;flex:1}.jsoneditor-modal .jsoneditor-jmespath-sort-field{width:170px}.jsoneditor-modal .jsoneditor-jmespath-sort-order{width:150px}.jsoneditor-modal .jsoneditor-jmespath-select-fields{width:100%}.jsoneditor-modal .selectr-selected{border-color:#d3d3d3;padding:4px 28px 4px 8px}.jsoneditor-modal .selectr-selected .selectr-tag{background-color:#3883fa;border-radius:5px}.jsoneditor-modal table td,.jsoneditor-modal table th{text-align:left;vertical-align:top;font-weight:400;color:#4d4d4d;border-spacing:0;border-collapse:collapse}.jsoneditor-modal #query,.jsoneditor-modal input,.jsoneditor-modal select,.jsoneditor-modal textarea{background:#fff;border:1px solid #d3d3d3;color:#4d4d4d;border-radius:3px;padding:4px}.jsoneditor-modal,.jsoneditor-modal #query,.jsoneditor-modal input,.jsoneditor-modal option,.jsoneditor-modal select,.jsoneditor-modal table td,.jsoneditor-modal table th,.jsoneditor-modal textarea{font-size:10.5pt;font-family:arial,sans-serif}.jsoneditor-modal #query,.jsoneditor-modal .jsoneditor-transform-preview{font-family:"dejavu sans mono","droid sans mono",consolas,monaco,"lucida console","courier new",courier,monospace,sans-serif;font-size:10pt;width:100%;box-sizing:border-box}.jsoneditor-modal input[type=button],.jsoneditor-modal input[type=submit]{background:#f5f5f5;padding:4px 20px}.jsoneditor-modal input,.jsoneditor-modal select{cursor:pointer}.jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-asc input.jsoneditor-button-asc,.jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-desc input.jsoneditor-button-desc{background:#3883fa;border-color:#3883fa;color:#fff}.jsoneditor-menu{width:100%;height:35px;padding:2px;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;background-color:#3883fa;border-bottom:1px solid #3883fa}.jsoneditor-menu>.jsoneditor-modes>button,.jsoneditor-menu>button{width:26px;height:26px;margin:2px;padding:0;border-radius:2px;border:1px solid transparent;background:transparent url(../img/jsoneditor-icons.svg);color:#fff;opacity:.8;font-family:arial,sans-serif;font-size:10pt;float:left}.jsoneditor-menu>.jsoneditor-modes>button:hover,.jsoneditor-menu>button:hover{background-color:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.4)}.jsoneditor-menu>.jsoneditor-modes>button:active,.jsoneditor-menu>.jsoneditor-modes>button:focus,.jsoneditor-menu>button:active,.jsoneditor-menu>button:focus{background-color:rgba(255,255,255,.3)}.jsoneditor-menu>.jsoneditor-modes>button:disabled,.jsoneditor-menu>button:disabled{opacity:.5;background-color:transparent;border:none}.jsoneditor-menu>button.jsoneditor-collapse-all{background-position:0 -96px}.jsoneditor-menu>button.jsoneditor-expand-all{background-position:0 -120px}.jsoneditor-menu>button.jsoneditor-sort{background-position:-120px -96px}.jsoneditor-menu>button.jsoneditor-transform{background-position:-144px -96px}.jsoneditor.jsoneditor-mode-form>.jsoneditor-menu>button.jsoneditor-sort,.jsoneditor.jsoneditor-mode-form>.jsoneditor-menu>button.jsoneditor-transform,.jsoneditor.jsoneditor-mode-view>.jsoneditor-menu>button.jsoneditor-sort,.jsoneditor.jsoneditor-mode-view>.jsoneditor-menu>button.jsoneditor-transform{display:none}.jsoneditor-menu>button.jsoneditor-undo{background-position:-24px -96px}.jsoneditor-menu>button.jsoneditor-undo:disabled{background-position:-24px -120px}.jsoneditor-menu>button.jsoneditor-redo{background-position:-48px -96px}.jsoneditor-menu>button.jsoneditor-redo:disabled{background-position:-48px -120px}.jsoneditor-menu>button.jsoneditor-compact{background-position:-72px -96px}.jsoneditor-menu>button.jsoneditor-format{background-position:-72px -120px}.jsoneditor-menu>button.jsoneditor-repair{background-position:-96px -96px}.jsoneditor-menu>.jsoneditor-modes{display:inline-block;float:left}.jsoneditor-menu>.jsoneditor-modes>button{background-image:none;width:auto;padding-left:6px;padding-right:6px}.jsoneditor-menu>.jsoneditor-modes>button.jsoneditor-separator,.jsoneditor-menu>button.jsoneditor-separator{margin-left:10px}.jsoneditor-menu a{font-family:arial,sans-serif;font-size:10pt;color:#fff;opacity:.8;vertical-align:middle}.jsoneditor-menu a:hover{opacity:1}.jsoneditor-menu a.jsoneditor-poweredBy{font-size:8pt;position:absolute;right:0;top:0;padding:10px}.jsoneditor-search{font-family:arial,sans-serif;position:absolute;right:4px;top:4px;border-collapse:collapse;border-spacing:0;display:flex}.jsoneditor-search input{color:#1a1a1a;width:120px;border:none;outline:0;margin:1px;line-height:20px}.jsoneditor-search button{width:16px;height:24px;padding:0;margin:0;border:none;background:url(../img/jsoneditor-icons.svg);vertical-align:top}.jsoneditor-search button:hover{background-color:transparent}.jsoneditor-search button.jsoneditor-refresh{width:18px;background-position:-99px -73px}.jsoneditor-search button.jsoneditor-next{cursor:pointer;background-position:-124px -73px}.jsoneditor-search button.jsoneditor-next:hover{background-position:-124px -49px}.jsoneditor-search button.jsoneditor-previous{cursor:pointer;background-position:-148px -73px;margin-right:2px}.jsoneditor-search button.jsoneditor-previous:hover{background-position:-148px -49px}.jsoneditor-results{font-family:arial,sans-serif;color:#fff;padding-right:5px;line-height:26px}.jsoneditor-frame{border:1px solid transparent;background-color:#fff;padding:0 2px;margin:0}.jsoneditor .autocomplete.dropdown{position:absolute;background:#fff;box-shadow:2px 2px 12px rgba(128,128,128,.3);border:1px solid #d3d3d3;z-index:100;overflow-x:hidden;overflow-y:auto;cursor:default;margin:0;padding:5px;text-align:left;outline:0;font-family:"dejavu sans mono","droid sans mono",consolas,monaco,"lucida console","courier new",courier,monospace,sans-serif;font-size:10pt}.jsoneditor .autocomplete.dropdown .item{color:#333}.jsoneditor .autocomplete.dropdown .item.hover{background-color:#ddd}.jsoneditor .autocomplete.hint{color:#aaa;top:4px;left:4px}.jsoneditor-treepath{padding:0 5px;overflow:hidden;white-space:nowrap;outline:0}.jsoneditor-treepath.show-all{word-wrap:break-word;white-space:normal;position:absolute;background-color:#ebebeb;z-index:999;box-shadow:2px 2px 12px rgba(128,128,128,.3)}.jsoneditor-treepath.show-all span.jsoneditor-treepath-show-all-btn{display:none}.jsoneditor-treepath div.jsoneditor-contextmenu-root{position:absolute;left:0}.jsoneditor-treepath .jsoneditor-treepath-show-all-btn{position:absolute;background-color:#ebebeb;left:0;height:20px;padding:0 3px;cursor:pointer}.jsoneditor-treepath .jsoneditor-treepath-element{margin:1px;font-family:arial,sans-serif;font-size:10pt}.jsoneditor-treepath .jsoneditor-treepath-seperator{margin:2px;font-size:9pt;font-family:arial,sans-serif}.jsoneditor-treepath span.jsoneditor-treepath-element:hover,.jsoneditor-treepath span.jsoneditor-treepath-seperator:hover{cursor:pointer;text-decoration:underline}.jsoneditor-statusbar{line-height:26px;height:26px;color:grey;background-color:#ebebeb;border-top:1px solid #d3d3d3;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:10pt}.jsoneditor-statusbar>.jsoneditor-curserinfo-val{margin-right:12px}.jsoneditor-statusbar>.jsoneditor-curserinfo-count{margin-left:4px}.jsoneditor-statusbar>.jsoneditor-validation-error-icon{float:right;width:24px;height:24px;padding:0;margin-top:1px;background:url(../img/jsoneditor-icons.svg) -168px -48px;cursor:pointer}.jsoneditor-statusbar>.jsoneditor-validation-error-count{float:right;margin:0 4px 0 0;cursor:pointer}.jsoneditor-statusbar>.jsoneditor-parse-error-icon{float:right;width:24px;height:24px;padding:0;margin:1px;background:url(../img/jsoneditor-icons.svg) -25px 0}.jsoneditor-statusbar .jsoneditor-array-info a{color:inherit}div.jsoneditor-statusbar>.jsoneditor-curserinfo-label,div.jsoneditor-statusbar>.jsoneditor-size-info{margin:0 4px}.jsoneditor-navigation-bar{width:100%;height:26px;line-height:26px;padding:0;margin:0;border-bottom:1px solid #d3d3d3;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:grey;background-color:#ebebeb;overflow:hidden;font-family:arial,sans-serif;font-size:10pt}/*! - * Selectr 2.4.0 - * https://github.com/Mobius1/Selectr - * - * Released under the MIT license - */.selectr-container{position:relative}.selectr-container li{list-style:none}.selectr-hidden{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0 none}.selectr-visible{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;z-index:11}.selectr-desktop.multiple .selectr-visible{display:none}.selectr-desktop.multiple.native-open .selectr-visible{top:100%;min-height:200px!important;height:auto;opacity:1;display:block}.selectr-container.multiple.selectr-mobile .selectr-selected{z-index:0}.selectr-selected{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:7px 28px 7px 14px;cursor:pointer;border:1px solid #999;border-radius:3px;background-color:#fff}.selectr-selected::before{position:absolute;top:50%;right:10px;width:0;height:0;content:'';-o-transform:rotate(0) translate3d(0,-50%,0);-ms-transform:rotate(0) translate3d(0,-50%,0);-moz-transform:rotate(0) translate3d(0,-50%,0);-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0);border-width:4px 4px 0 4px;border-style:solid;border-color:#6c7a86 transparent transparent}.selectr-container.native-open .selectr-selected::before,.selectr-container.open .selectr-selected::before{border-width:0 4px 4px 4px;border-style:solid;border-color:transparent transparent #6c7a86}.selectr-label{display:none;overflow:hidden;width:100%;white-space:nowrap;text-overflow:ellipsis}.selectr-placeholder{color:#6c7a86}.selectr-tags{margin:0;padding:0;white-space:normal}.has-selected .selectr-tags{margin:0 0 -2px}.selectr-tag{list-style:none;position:relative;float:left;padding:2px 25px 2px 8px;margin:0 2px 2px 0;cursor:default;color:#fff;border:medium none;border-radius:10px;background:#acb7bf none repeat scroll 0 0}.selectr-container.multiple.has-selected .selectr-selected{padding:5px 28px 5px 5px}.selectr-options-container{position:absolute;z-index:10000;top:calc(100% - 1px);left:0;display:none;box-sizing:border-box;width:100%;border-width:0 1px 1px;border-style:solid;border-color:transparent #999 #999;border-radius:0 0 3px 3px;background-color:#fff}.selectr-container.open .selectr-options-container{display:block}.selectr-input-container{position:relative;display:none}.selectr-clear,.selectr-input-clear,.selectr-tag-remove{position:absolute;top:50%;right:22px;width:20px;height:20px;padding:0;cursor:pointer;-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border:medium none;background-color:transparent;z-index:11}.selectr-clear,.selectr-input-clear{display:none}.selectr-container.has-selected .selectr-clear,.selectr-input-container.active .selectr-input-clear{display:block}.selectr-selected .selectr-tag-remove{right:2px}.selectr-clear::after,.selectr-clear::before,.selectr-input-clear::after,.selectr-input-clear::before,.selectr-tag-remove::after,.selectr-tag-remove::before{position:absolute;top:5px;left:9px;width:2px;height:10px;content:' ';background-color:#6c7a86}.selectr-tag-remove::after,.selectr-tag-remove::before{top:4px;width:3px;height:12px;background-color:#fff}.selectr-clear:before,.selectr-input-clear::before,.selectr-tag-remove::before{-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.selectr-clear:after,.selectr-input-clear::after,.selectr-tag-remove::after{-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.selectr-input-container.active,.selectr-input-container.active .selectr-clear{display:block}.selectr-input{top:5px;left:5px;box-sizing:border-box;width:calc(100% - 30px);margin:10px 15px;padding:7px 30px 7px 9px;border:1px solid #999;border-radius:3px}.selectr-notice{display:none;box-sizing:border-box;width:100%;padding:8px 16px;border-top:1px solid #999;border-radius:0 0 3px 3px;background-color:#fff}.selectr-container.notice .selectr-notice{display:block}.selectr-container.notice .selectr-selected{border-radius:3px 3px 0 0}.selectr-options{position:relative;top:calc(100% + 2px);display:none;overflow-x:auto;overflow-y:scroll;max-height:200px;margin:0;padding:0}.selectr-container.notice .selectr-options-container,.selectr-container.open .selectr-input-container,.selectr-container.open .selectr-options{display:block}.selectr-option{position:relative;display:block;padding:5px 20px;list-style:outside none none;cursor:pointer;font-weight:400}.selectr-options.optgroups>.selectr-option{padding-left:25px}.selectr-optgroup{font-weight:700;padding:0}.selectr-optgroup--label{font-weight:700;margin-top:10px;padding:5px 15px}.selectr-match{text-decoration:underline}.selectr-option.selected{background-color:#ddd}.selectr-option.active{color:#fff;background-color:#5897fb}.selectr-option.disabled{opacity:.4}.selectr-option.excluded{display:none}.selectr-container.open .selectr-selected{border-color:#999 #999 transparent #999;border-radius:3px 3px 0 0}.selectr-container.open .selectr-selected::after{-o-transform:rotate(180deg) translate3d(0,50%,0);-ms-transform:rotate(180deg) translate3d(0,50%,0);-moz-transform:rotate(180deg) translate3d(0,50%,0);-webkit-transform:rotate(180deg) translate3d(0,50%,0);transform:rotate(180deg) translate3d(0,50%,0)}.selectr-disabled{opacity:.6}.has-selected .selectr-placeholder,.selectr-empty{display:none}.has-selected .selectr-label{display:block}.taggable .selectr-selected{padding:4px 28px 4px 4px}.taggable .selectr-selected::after{display:table;content:" ";clear:both}.taggable .selectr-label{width:auto}.taggable .selectr-tags{float:left;display:block}.taggable .selectr-placeholder{display:none}.input-tag{float:left;min-width:90px;width:auto}.selectr-tag-input{border:medium none;padding:3px 10px;width:100%;font-family:inherit;font-weight:inherit;font-size:inherit}.selectr-input-container.loading::after{position:absolute;top:50%;right:20px;width:20px;height:20px;content:'';-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);-o-transform-origin:50% 0 0;-ms-transform-origin:50% 0 0;-moz-transform-origin:50% 0 0;-webkit-transform-origin:50% 0 0;transform-origin:50% 0 0;-moz-animation:.5s linear 0s normal forwards infinite running spin;-webkit-animation:.5s linear 0s normal forwards infinite running spin;animation:.5s linear 0s normal forwards infinite running spin;border-width:3px;border-style:solid;border-color:#aaa #ddd #ddd;border-radius:50%}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}@keyframes spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}.selectr-container.open.inverted .selectr-selected{border-color:transparent #999 #999;border-radius:0 0 3px 3px}.selectr-container.inverted .selectr-options-container{border-width:1px 1px 0;border-color:#999 #999 transparent;border-radius:3px 3px 0 0;background-color:#fff}.selectr-container.inverted .selectr-options-container{top:auto;bottom:calc(100% - 1px)}.selectr-container ::-webkit-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::-moz-placeholder{color:#6c7a86;opacity:1}.selectr-container :-ms-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::placeholder{color:#6c7a86;opacity:1} \ No newline at end of file diff --git a/demo/gdml/src/jsMain/resources/css/main.css b/demo/gdml/src/jsMain/resources/css/main.css deleted file mode 100644 index 9eb3f7fd..00000000 --- a/demo/gdml/src/jsMain/resources/css/main.css +++ /dev/null @@ -1,52 +0,0 @@ -.drop_zone { - outline: 1px solid orange; -} - -.loader { - border: 16px solid #f3f3f3; /* Light grey */ - border-top: 16px solid #3498db; /* Blue */ - border-radius: 50%; - width: 120px; - height: 120px; - animation: spin 2s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Remove default bullets */ -ul, .tree { - list-style-type: none; -} - -/* Style the caret/arrow */ -.tree-caret { - cursor: pointer; - user-select: none; /* Prevent text selection */ -} - -.objTree-label { - cursor: pointer; -} - -/* Create the caret/arrow with a unicode, and style it */ -.tree-caret::before { - content: "\25B6"; - color: black; - display: inline-block; - margin-right: 6px; -} - -.objTree-leaf::before { - content: "\25C6"; - color: black; - display: inline-block; - margin-right: 6px; -} - -/* Rotate the caret/arrow icon when clicked on (using JavaScript) */ -.tree-caret-down::before { - transform: rotate(90deg); -} diff --git a/demo/gdml/src/jsMain/resources/img/jsoneditor-icons.svg b/demo/gdml/src/jsMain/resources/img/jsoneditor-icons.svg deleted file mode 100644 index 332be179..00000000 --- a/demo/gdml/src/jsMain/resources/img/jsoneditor-icons.svg +++ /dev/null @@ -1,748 +0,0 @@ - - - JSON Editor Icons - - - - image/svg+xml - - JSON Editor Icons - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demo/gdml/src/jsMain/resources/index.html b/demo/gdml/src/jsMain/resources/index.html index 6a8a8ebe..1a503224 100644 --- a/demo/gdml/src/jsMain/resources/index.html +++ b/demo/gdml/src/jsMain/resources/index.html @@ -4,45 +4,14 @@ Three js demo for particle physics - - - + + + + + - -
-

GDML demo

-
- -
-
-
-
-
- Load file: - -
-
-
-
- Load data -
- (drag file here) -
-
-
-
-
-
-
-
-
-
-
-
-
- - - +
+ \ No newline at end of file diff --git a/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt b/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt index 9de54bc5..b3b6dc90 100644 --- a/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt +++ b/demo/gdml/src/jvmMain/kotlin/hep/dataforge/vis/spatial/gdml/demo/GDMLDemoApp.kt @@ -5,9 +5,9 @@ import hep.dataforge.vis.editor.VisualObjectEditorFragment import hep.dataforge.vis.editor.VisualObjectTreeFragment import hep.dataforge.vis.spatial.Material3D import hep.dataforge.vis.spatial.Visual3D -import hep.dataforge.vis.spatial.VisualGroup3D import hep.dataforge.vis.spatial.fx.FX3DPlugin import hep.dataforge.vis.spatial.fx.FXCanvas3D +import hep.dataforge.vis.spatial.gdml.toVisual import javafx.geometry.Orientation import javafx.scene.Parent import javafx.stage.FileChooser @@ -36,10 +36,15 @@ class GDMLView : View() { buttonbar { button("Load GDML/json") { action { - val file = chooseFile("Select a GDML/json file", filters = fileNameFilter).firstOrNull() - ?: return@action - val visual: VisualGroup3D = Visual3D.readFile(file) - canvas.render(visual) + runAsync { + val file = chooseFile("Select a GDML/json file", filters = fileNameFilter).firstOrNull() + ?: return@runAsync null + Visual3D.readFile(file) + } ui { + if (it != null) { + canvas.render(it) + } + } } } } @@ -51,6 +56,14 @@ class GDMLView : View() { } } + init { + runAsync { + cubes().toVisual() + } ui { + canvas.render(it) + } + } + companion object { private val fileNameFilter = arrayOf( FileChooser.ExtensionFilter("GDML", "*.gdml", "*.xml"), diff --git a/demo/muon-monitor/build.gradle.kts b/demo/muon-monitor/build.gradle.kts index b2290bb1..42ea975e 100644 --- a/demo/muon-monitor/build.gradle.kts +++ b/demo/muon-monitor/build.gradle.kts @@ -1,3 +1,5 @@ +import kotlinx.atomicfu.plugin.gradle.withKotlinTargets +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME import scientifik.jsDistDirectory plugins { @@ -13,7 +15,7 @@ kotlin { val installJS = tasks.getByName("jsBrowserDistribution") - js{ + js { browser { dceTask { dceOptions { @@ -28,14 +30,15 @@ kotlin { jvm { withJava() - compilations.findByName("main").apply { - tasks.getByName("jvmProcessResources") { + compilations[MAIN_COMPILATION_NAME]?.apply { + tasks.getByName(processResourcesTaskName) { dependsOn(installJS) afterEvaluate { from(project.jsDistDirectory) } } } + } sourceSets { @@ -53,6 +56,7 @@ kotlin { } jsMain { dependencies { + implementation(project(":ui:bootstrap")) implementation("io.ktor:ktor-client-js:$ktorVersion") implementation("io.ktor:ktor-client-serialization-js:$ktorVersion") implementation(npm("text-encoding")) @@ -70,17 +74,4 @@ kotlin { application { mainClassName = "ru.mipt.npm.muon.monitor.server.MMServerKt" -} - -//configure { -// modules("javafx.controls") -//} - -val common = project(":dataforge-vis-common") - -val copyJsResourcesFromCommon by tasks.creating(Copy::class){ - from(common.buildDir.resolve("processedResources\\js\\main\\")) - into(buildDir.resolve("processedResources\\js\\main\\")) -} - -tasks.getByPath("jsProcessResources").dependsOn(copyJsResourcesFromCommon) \ No newline at end of file +} \ No newline at end of file diff --git a/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt b/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt index b76be164..358a1c04 100644 --- a/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt +++ b/demo/muon-monitor/src/commonMain/kotlin/ru/mipt/npm/muon/monitor/Model.kt @@ -62,6 +62,7 @@ class Model { fun reset() { map.values.forEach { + it.config it.setProperty(Material3D.MATERIAL_COLOR_KEY, null) } tracks.removeAll() diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt index bf05be52..d7a0a3af 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt @@ -1,15 +1,15 @@ package ru.mipt.npm.muon.monitor import hep.dataforge.context.Context -import hep.dataforge.js.card -import hep.dataforge.js.component -import hep.dataforge.js.state import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.isEmpty import hep.dataforge.vis.VisualObject -import hep.dataforge.vis.editor.configEditor -import hep.dataforge.vis.editor.objectTree +import hep.dataforge.vis.bootstrap.card +import hep.dataforge.vis.bootstrap.configEditor +import hep.dataforge.vis.bootstrap.objectTree +import hep.dataforge.vis.react.component +import hep.dataforge.vis.react.state import hep.dataforge.vis.spatial.specifications.Camera import hep.dataforge.vis.spatial.specifications.Canvas import hep.dataforge.vis.spatial.three.ThreeCanvas @@ -48,13 +48,14 @@ val MMApp = component { props -> } val visual = props.model.root + div("row") { h1("mx-auto") { - +"Muon monitor demo" + +"GDML/JSON render demo" } } div("row") { - div("col-lg-3 mh-100 px-0") { + div("col-lg-3 px-0 overflow-auto") { //tree card("Object tree") { objectTree(visual, selected, select) @@ -154,7 +155,7 @@ val MMApp = component { props -> else -> visual[selected] } if (selectedObject != null) { - configEditor(selectedObject, default = selectedObject.allProperties()) + configEditor(selectedObject, default = selectedObject.allProperties(), key = selected) } } } diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt index 86482004..50792b82 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt @@ -9,6 +9,7 @@ import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.serializer.KotlinxSerializer import kotlinx.serialization.json.Json import react.child +import react.dom.div import react.dom.render import kotlin.browser.document @@ -30,11 +31,13 @@ private class MMDemoApp : Application { val element = document.getElementById("app") ?: error("Element with id 'app' not found on page") render(element) { - child(MMApp) { - attrs { - model = this@MMDemoApp.model - connection = this@MMDemoApp.connection - this.context = context + div("container-fluid h-100") { + child(MMApp) { + attrs { + model = this@MMDemoApp.model + connection = this@MMDemoApp.connection + this.context = context + } } } } diff --git a/demo/spatial-showcase/build.gradle.kts b/demo/spatial-showcase/build.gradle.kts index 064f207d..f83aaaa1 100644 --- a/demo/spatial-showcase/build.gradle.kts +++ b/demo/spatial-showcase/build.gradle.kts @@ -1,6 +1,4 @@ -import scientifik.DependencyConfiguration -import scientifik.FXModule -import scientifik.fx +import scientifik.* plugins { id("scientifik.mpp") @@ -8,7 +6,7 @@ plugins { } val fxVersion: String by rootProject.extra -fx(FXModule.CONTROLS, version = fxVersion, configuration = DependencyConfiguration.IMPLEMENTATION) +useFx(FXModule.CONTROLS, version = fxVersion, configuration = DependencyConfiguration.IMPLEMENTATION) kotlin { diff --git a/playground/build.gradle.kts b/playground/build.gradle.kts index 3ad5ff15..5c53e40d 100644 --- a/playground/build.gradle.kts +++ b/playground/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { dependencies { api(project(":dataforge-vis-spatial")) api(project(":dataforge-vis-spatial-gdml")) + api(project(":ui:bootstrap")) } } } diff --git a/playground/src/jsMain/kotlin/PlayGroundApp.kt b/playground/src/jsMain/kotlin/PlayGroundApp.kt index 79efa165..59a52021 100644 --- a/playground/src/jsMain/kotlin/PlayGroundApp.kt +++ b/playground/src/jsMain/kotlin/PlayGroundApp.kt @@ -2,8 +2,8 @@ import hep.dataforge.context.Global import hep.dataforge.js.Application import hep.dataforge.js.startApplication import hep.dataforge.names.Name -import hep.dataforge.vis.editor.objectTree -import hep.dataforge.vis.editor.visualPropertyEditor +import hep.dataforge.vis.bootstrap.objectTree +import hep.dataforge.vis.bootstrap.visualPropertyEditor import hep.dataforge.vis.spatial.Point3D import hep.dataforge.vis.spatial.VisualGroup3D import hep.dataforge.vis.spatial.box @@ -45,8 +45,6 @@ private class PlayGroundApp : Application { } } } - - } } diff --git a/settings.gradle.kts b/settings.gradle.kts index b6673c37..636071e3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,11 @@ pluginManagement { rootProject.name = "dataforge-vis" include( + ":ui", + ":ui:react", + ":ui:ring", + ":ui:material", + ":ui:bootstrap", ":dataforge-vis-common", ":dataforge-vis-spatial", ":dataforge-vis-spatial-gdml", @@ -33,13 +38,3 @@ include( ":demo:muon-monitor", ":playground" ) - -//if(file("../dataforge-core").exists()) { -// includeBuild("../dataforge-core"){ -// dependencySubstitution { -// //substitute(module("hep.dataforge:dataforge-output")).with(project(":dataforge-output")) -// substitute(module("hep.dataforge:dataforge-output-jvm")).with(project(":dataforge-output")) -// substitute(module("hep.dataforge:dataforge-output-js")).with(project(":dataforge-output")) -// } -// } -//} \ No newline at end of file diff --git a/ui/bootstrap/build.gradle.kts b/ui/bootstrap/build.gradle.kts new file mode 100644 index 00000000..9a974d8d --- /dev/null +++ b/ui/bootstrap/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("scientifik.js") +} + +val dataforgeVersion: String by rootProject.extra + +kotlin { + target { + useCommonJs() + } +} + +dependencies{ + api(project(":dataforge-vis-common")) + api(project(":ui:react")) +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/MetaViewerComponent.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/MetaViewerComponent.kt similarity index 98% rename from dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/MetaViewerComponent.kt rename to ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/MetaViewerComponent.kt index 75e99c7c..0a287e07 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/MetaViewerComponent.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/MetaViewerComponent.kt @@ -1,4 +1,4 @@ -package hep.dataforge.vis.editor +package hep.dataforge.vis.bootstrap import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaItem diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/ObjectTree.kt similarity index 93% rename from dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt rename to ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/ObjectTree.kt index af4faaa9..1862a0bb 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/ObjectTree.kt @@ -1,15 +1,13 @@ -package hep.dataforge.vis.editor +package hep.dataforge.vis.bootstrap -import hep.dataforge.js.RFBuilder -import hep.dataforge.js.card -import hep.dataforge.js.component -import hep.dataforge.js.state import hep.dataforge.names.Name import hep.dataforge.names.plus import hep.dataforge.names.startsWith import hep.dataforge.vis.VisualGroup import hep.dataforge.vis.VisualObject import hep.dataforge.vis.isEmpty +import hep.dataforge.vis.react.RFBuilder +import hep.dataforge.vis.react.component import kotlinx.html.classes import kotlinx.html.js.onClickFunction import org.w3c.dom.Element @@ -29,7 +27,7 @@ interface TreeState : RState { } private fun RFBuilder.objectTree(props: ObjectTreeProps): Unit { - var expanded: Boolean by state{ props.selected?.startsWith(props.name) ?: false } + var expanded: Boolean by useState{ props.selected?.startsWith(props.name) ?: false } val onClick: (Event) -> Unit = { expanded = !expanded diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/bootstrap.kt similarity index 57% rename from dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt rename to ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/bootstrap.kt index 9c3b15f7..4a0d5a42 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/bootstrap.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/bootstrap.kt @@ -1,9 +1,13 @@ -package hep.dataforge.js +package hep.dataforge.vis.bootstrap +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken import kotlinx.html.* import kotlinx.html.js.div +import kotlinx.html.js.onClickFunction import org.w3c.dom.HTMLElement import react.RBuilder +import react.ReactElement import react.dom.* inline fun TagConsumer.card(title: String, crossinline block: TagConsumer.() -> Unit) { @@ -16,7 +20,7 @@ inline fun TagConsumer.card(title: String, crossinline block: TagCo } inline fun RBuilder.card(title: String, crossinline block: RBuilder.() -> Unit) { - div("card w-100 h-100") { + div("card w-100") { div("card-body") { h3(classes = "card-title") { +title @@ -26,7 +30,6 @@ inline fun RBuilder.card(title: String, crossinline block: RBuilder.() -> Unit) } } - fun TagConsumer.accordion(id: String, elements: List Unit>>) { div("container-fluid") { div("accordion") { @@ -59,7 +62,6 @@ fun TagConsumer.accordion(id: String, elements: List Unit>> fun AccordionBuilder.entry(title: String, builder: DIV.() -> Unit) { @@ -71,8 +73,8 @@ fun TagConsumer.accordion(id: String, builder: AccordionBuilder.() accordion(id, list) } -fun RBuilder.accordion(id: String, elements: List.() -> Unit>>) { - div("container-fluid") { +fun RBuilder.accordion(id: String, elements: List.() -> Unit>>): ReactElement { + return div("container-fluid") { div("accordion") { attrs { this.id = id @@ -111,13 +113,102 @@ fun RBuilder.accordion(id: String, elements: List. } } +fun RBuilder.namecrumbs(name: Name?, rootTitle: String, link: (Name) -> Unit) { + div("container-fluid p-0") { + nav { + attrs { + attributes["aria-label"] = "breadcrumb" + } + ol("breadcrumb") { + li("breadcrumb-item") { + button(classes = "btn btn-link p-0") { + +rootTitle + attrs { + onClickFunction = { + link(Name.EMPTY) + } + } + } + } + if (name != null) { + val tokens = ArrayList(name.length) + name.tokens.forEach { token -> + tokens.add(token) + val fullName = Name(tokens.toList()) + li("breadcrumb-item") { + button(classes = "btn btn-link p-0") { + +token.toString() + attrs { + onClickFunction = { + console.log("Selected = $fullName") + link(fullName) + } + } + } + } + } + } + } + } + } +} + typealias RAccordionBuilder = MutableList.() -> Unit>> fun RAccordionBuilder.entry(title: String, builder: RDOMBuilder
.() -> Unit) { add(title to builder) } -fun RBuilder.accordion(id: String, builder: RAccordionBuilder.() -> Unit) { +fun RBuilder.accordion(id: String, builder: RAccordionBuilder.() -> Unit): ReactElement { val list = ArrayList.() -> Unit>>().apply(builder) - accordion(id, list) + return accordion(id, list) +} + +fun joinStyles(vararg styles: String?) = styles.joinToString(separator = " ") { it ?: "" } + +inline fun RBuilder.flexColumn(classes: String? = null, block: RDOMBuilder
.() -> Unit) = + div(joinStyles(classes, "d-flex flex-column"), block) + +inline fun RBuilder.flexRow(classes: String? = null, block: RDOMBuilder
.() -> Unit) = + div(joinStyles(classes, "d-flex flex-row"), block) + +enum class ContainerSize(val suffix: String) { + DEFAULT(""), + SM("-sm"), + MD("-md"), + LG("-lg"), + XL("-xl"), + FLUID("-fluid") +} + +inline fun RBuilder.container( + classes: String? = null, + size: ContainerSize = ContainerSize.FLUID, + block: RDOMBuilder
.() -> Unit +): ReactElement = div(joinStyles(classes, "container${size.suffix}"), block) + + +enum class GridMaxSize(val suffix: String) { + NONE(""), + SM("-sm"), + MD("-md"), + LG("-lg"), + XL("-xl") +} + +inline fun RBuilder.gridColumn( + weight: Int? = null, + classes: String? = null, + maxSize: GridMaxSize = GridMaxSize.NONE, + block: RDOMBuilder
.() -> Unit +): ReactElement { + val weightSuffix = weight?.let { "-$it" } ?: "" + return div(joinStyles(classes, "col${maxSize.suffix}$weightSuffix"), block) +} + +inline fun RBuilder.gridRow( + classes: String? = null, + block: RDOMBuilder
.() -> Unit +): ReactElement { + return div(joinStyles(classes, "row"), block) } \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/configEditor.kt similarity index 50% rename from dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt rename to ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/configEditor.kt index 6da696f6..ccbf69df 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/configEditor.kt @@ -1,13 +1,14 @@ -package hep.dataforge.vis.editor +package hep.dataforge.vis.bootstrap -import hep.dataforge.js.RFBuilder -import hep.dataforge.js.component -import hep.dataforge.js.state import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.* import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.plus +import hep.dataforge.values.Value +import hep.dataforge.vis.react.RFBuilder +import hep.dataforge.vis.react.component +import hep.dataforge.vis.react.state import kotlinx.html.classes import kotlinx.html.js.onClickFunction import org.w3c.dom.Element @@ -15,7 +16,7 @@ import org.w3c.dom.events.Event import react.* import react.dom.* -interface ConfigEditorProps : RProps { +interface ConfigEditorItemProps : RProps { /** * Root config object - always non null @@ -38,19 +39,22 @@ interface ConfigEditorProps : RProps { var descriptor: NodeDescriptor? } -private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { +private val ConfigEditorItem: FunctionalComponent = component { props -> + configEditorItem(props) +} + +private fun RFBuilder.configEditorItem(props: ConfigEditorItemProps) { var expanded: Boolean by state { true } - val item = props.root[props.name] + var item: MetaItem? by state { props.root[props.name] } val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name) val defaultItem = props.default?.get(props.name) - val actualItem: MetaItem? = item ?: defaultItem ?: descriptorItem?.defaultItem() + var actualItem: MetaItem? by state { item ?: defaultItem ?: descriptorItem?.defaultItem() } val token = props.name.last()?.toString() ?: "Properties" - var kostyl by state { false } - fun update() { - kostyl = !kostyl + item = props.root[props.name] + actualItem = item ?: defaultItem ?: descriptorItem?.defaultItem() } useEffectWithCleanup(listOf(props.root)) { @@ -66,6 +70,15 @@ private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { expanded = !expanded } + val valueChanged: (Value?) -> Unit = { + if (it == null) { + props.root.remove(props.name) + } else { + props.root[props.name] = it + } + update() + } + val removeClick: (Event) -> Unit = { props.root.remove(props.name) update() @@ -103,7 +116,16 @@ private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { keys.forEach { token -> li("tree-item align-middle") { - configEditor(props.root, props.name + token, props.descriptor, props.default) + child(ConfigEditorItem) { + attrs { + this.key = props.name.toString() + this.root = props.root + this.name = props.name + token + this.default = props.default + this.descriptor = props.descriptor + } + } + //configEditor(props.root, props.name + token, props.descriptor, props.default) } } } @@ -111,20 +133,27 @@ private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { } is MetaItem.ValueItem -> { div { - div("d-flex flex-row align-items-center") { - div("flex-grow-1 p-1 mr-auto tree-label") { - +token - attrs { - if (item == null) { - classes += "tree-label-inactive" + div("row form-group") { + div("col d-inline-flex") { + span("tree-label align-self-center") { + +token + attrs { + if (item == null) { + classes += "tree-label-inactive" + } } } } - div("d-inline-flex") { - valueChooser(props.root, props.name, actualItem.value, descriptorItem as? ValueDescriptor) + div("col-6 d-inline-flex") { + valueChooser( + props.name, + actualItem, + descriptorItem as? ValueDescriptor, + valueChanged + ) } - div("d-inline-flex p-1") { - button(classes = "btn btn-link") { + div("col-auto d-inline-flex p-1") { + button(classes = "btn btn-link align-self-center") { +"\u00D7" attrs { if (item == null) { @@ -141,30 +170,55 @@ private fun RFBuilder.configEditorItem(props: ConfigEditorProps) { } } -val ConfigEditor: FunctionalComponent = component { configEditorItem(it) } +interface ConfigEditorProps : RProps { + var id: Name + var root: Config + var default: Meta? + var descriptor: NodeDescriptor? +} +val ConfigEditor = component { props -> + child(ConfigEditorItem) { + attrs { + this.key = "" + this.root = props.root + this.name = Name.EMPTY + this.default = props.default + this.descriptor = props.descriptor + } + } +} -fun RBuilder.configEditor( - config: Config, - name: Name = Name.EMPTY, - descriptor: NodeDescriptor? = null, - default: Meta? = null -) { +fun Element.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null, key: Any? = null) { + render(this) { + child(ConfigEditor) { + attrs { + this.key = key?.toString() ?: "" + this.root = config + this.descriptor = descriptor + this.default = default + } + } + } +} + +fun RBuilder.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null, key: Any? = null) { child(ConfigEditor) { attrs { + this.key = key?.toString() ?: "" this.root = config - this.name = name this.descriptor = descriptor this.default = default } } } -fun Element.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null) { - render(this) { - configEditor(config, Name.EMPTY, descriptor, default) +fun RBuilder.configEditor(obj: Configurable, descriptor: NodeDescriptor? = obj.descriptor, default: Meta? = null, key: Any? = null) { + child(ConfigEditor) { + attrs { + this.key = key?.toString() ?: "" + this.root = obj.config + this.descriptor = descriptor + this.default = default + } } } - -fun RBuilder.configEditor(obj: Configurable, descriptor: NodeDescriptor? = obj.descriptor, default: Meta? = null) { - configEditor(obj.config, Name.EMPTY, descriptor ?: obj.descriptor, default) -} diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/propertyEditor.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/propertyEditor.kt similarity index 95% rename from dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/propertyEditor.kt rename to ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/propertyEditor.kt index 00f928ee..d4b69c48 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/propertyEditor.kt +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/propertyEditor.kt @@ -1,6 +1,5 @@ -package hep.dataforge.vis.editor +package hep.dataforge.vis.bootstrap -import hep.dataforge.js.card import hep.dataforge.meta.Meta import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.names.Name diff --git a/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/valueChooser.kt b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/valueChooser.kt new file mode 100644 index 00000000..d49cd552 --- /dev/null +++ b/ui/bootstrap/src/main/kotlin/hep/dataforge/vis/bootstrap/valueChooser.kt @@ -0,0 +1,182 @@ +package hep.dataforge.vis.bootstrap + +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.descriptors.ValueDescriptor +import hep.dataforge.meta.get +import hep.dataforge.meta.string +import hep.dataforge.meta.value +import hep.dataforge.names.Name +import hep.dataforge.values.* +import hep.dataforge.vis.Colors +import hep.dataforge.vis.widgetType +import kotlinx.html.InputType +import kotlinx.html.js.onChangeFunction +import kotlinx.html.js.onKeyDownFunction +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLSelectElement +import org.w3c.dom.events.Event +import org.w3c.dom.events.KeyboardEvent +import react.* +import react.dom.div +import react.dom.input +import react.dom.option +import react.dom.select + +interface ValueChooserProps : RProps { + var item: MetaItem<*>? + var descriptor: ValueDescriptor? + var valueChanged: ((Value?) -> Unit)? +} + +interface ValueChooserState : RState { + var value: Value? + var rawInput: Boolean? +} + +class ValueChooserComponent(props: ValueChooserProps) : RComponent(props) { + private val element = createRef() + + override fun ValueChooserState.init(props: ValueChooserProps) { + value = props.item.value + } + + private fun getValue(): Value? { + val element = element.current ?: return null//state.element ?: return null + return when (element) { + is HTMLInputElement -> if (element.type == "checkbox") { + if (element.checked) True else False + } else { + element.value.asValue() + } + is HTMLSelectElement -> element.value.asValue() + else -> error("Unknown event target: $element") + } + } + + private val valueChanged: (Event) -> Unit = { _ -> + setState { + value = getValue() + } + } + + private val valueChangeAndCommit: (Event) -> Unit = { event -> + val res = getValue() + setState { + value = res + } + props.valueChanged?.invoke(res) + + } + + private val keyDown: (Event) -> Unit = { event -> + if (event is KeyboardEvent && event.key == "Enter") { + props.valueChanged?.invoke(getValue()) + } + } + + override fun shouldComponentUpdate( + nextProps: ValueChooserProps, + nextState: ValueChooserState + ): Boolean = nextProps.item !== props.item + + override fun componentDidUpdate(prevProps: ValueChooserProps, prevState: ValueChooserState, snapshot: Any) { + (element.current as? HTMLInputElement)?.let { element -> + if (element.type == "checkbox") { + element.checked = state.value?.boolean ?: false + } else { + element.value = state.value?.string ?: "" + } + element.indeterminate = state.value == null + } +// (state.element as? HTMLSelectElement)?.let { element -> +// state.value?.let { element.value = it.string } +// } + } + + private fun RBuilder.stringInput() = input(type = InputType.text) { + attrs { + this.value = state.value?.string ?: "" + onChangeFunction = valueChanged + onKeyDownFunction = keyDown + } + ref = element + } + + override fun RBuilder.render() { + div("align-self-center") { + val descriptor = props.descriptor + val type = descriptor?.type?.firstOrNull() + when { + state.rawInput == true -> stringInput() + descriptor?.widgetType == "color" -> input(type = InputType.color) { + ref = element + attrs { + this.value = state.value?.let { value -> + if (value.type == ValueType.NUMBER) Colors.rgbToString(value.int) + else value.string + } ?: "#000000" + onChangeFunction = valueChangeAndCommit + } + } + type == ValueType.BOOLEAN -> { + input(type = InputType.checkBox) { + ref = element + attrs { + checked = state.value?.boolean ?: false + onChangeFunction = valueChangeAndCommit + } + } + } + type == ValueType.NUMBER -> input(type = InputType.number) { + ref = element + attrs { + descriptor.attributes["step"].string?.let { + step = it + } + descriptor.attributes["min"].string?.let { + min = it + } + descriptor.attributes["max"].string?.let { + max = it + } + this.value = state.value?.string ?: "" + onChangeFunction = valueChanged + onKeyDownFunction = keyDown + } + } + descriptor?.allowedValues?.isNotEmpty() ?: false -> select { + descriptor!!.allowedValues.forEach { + option { + +it.string + } + } + ref = element + attrs { + this.value = state.value?.string ?: "" + multiple = false + onChangeFunction = valueChangeAndCommit + } + } + else -> stringInput() + } + } + } + +} + +internal fun RBuilder.valueChooser( + name: Name, + item: MetaItem<*>?, + descriptor: ValueDescriptor? = null, + callback: (Value?) -> Unit +) { + child(ValueChooserComponent::class) { + attrs { + key = name.toString() + this.item = item + this.descriptor = descriptor + this.valueChanged = callback + } + } +} diff --git a/dataforge-vis-common/src/jsMain/resources/css/bootstrap.min.css b/ui/bootstrap/src/main/resources/css/bootstrap.min.css similarity index 100% rename from dataforge-vis-common/src/jsMain/resources/css/bootstrap.min.css rename to ui/bootstrap/src/main/resources/css/bootstrap.min.css diff --git a/dataforge-vis-common/src/jsMain/resources/css/bootstrap.min.css.map b/ui/bootstrap/src/main/resources/css/bootstrap.min.css.map similarity index 100% rename from dataforge-vis-common/src/jsMain/resources/css/bootstrap.min.css.map rename to ui/bootstrap/src/main/resources/css/bootstrap.min.css.map diff --git a/dataforge-vis-common/src/jsMain/resources/css/main.css b/ui/bootstrap/src/main/resources/css/main.css similarity index 100% rename from dataforge-vis-common/src/jsMain/resources/css/main.css rename to ui/bootstrap/src/main/resources/css/main.css diff --git a/dataforge-vis-common/src/jsMain/resources/js/bootstrap.bundle.min.js b/ui/bootstrap/src/main/resources/js/bootstrap.bundle.min.js similarity index 100% rename from dataforge-vis-common/src/jsMain/resources/js/bootstrap.bundle.min.js rename to ui/bootstrap/src/main/resources/js/bootstrap.bundle.min.js diff --git a/dataforge-vis-common/src/jsMain/resources/js/bootstrap.bundle.min.js.map b/ui/bootstrap/src/main/resources/js/bootstrap.bundle.min.js.map similarity index 100% rename from dataforge-vis-common/src/jsMain/resources/js/bootstrap.bundle.min.js.map rename to ui/bootstrap/src/main/resources/js/bootstrap.bundle.min.js.map diff --git a/dataforge-vis-common/src/jsMain/resources/js/jquery-3.4.1.min.js b/ui/bootstrap/src/main/resources/js/jquery-3.4.1.min.js similarity index 100% rename from dataforge-vis-common/src/jsMain/resources/js/jquery-3.4.1.min.js rename to ui/bootstrap/src/main/resources/js/jquery-3.4.1.min.js diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts new file mode 100644 index 00000000..e69de29b diff --git a/ui/material/build.gradle.kts b/ui/material/build.gradle.kts new file mode 100644 index 00000000..459380c5 --- /dev/null +++ b/ui/material/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("scientifik.js") +} + +val dataforgeVersion: String by rootProject.extra + +kotlin { + target { + useCommonJs() + } +} + +dependencies{ + api(project(":dataforge-vis-common")) + api(project(":ui:react")) + + api("subroh0508.net.kotlinmaterialui:core:0.3.16") + api("subroh0508.net.kotlinmaterialui:lab:0.3.16") + api(npm("@material-ui/core","4.9.13")) + api(npm("@material-ui/lab","4.0.0-alpha.52")) + //api(npm("@material-ui/icons","4.9.1")) +} \ No newline at end of file diff --git a/ui/material/src/main/kotlin/hep/dataforge/vis/material/configEditor.kt b/ui/material/src/main/kotlin/hep/dataforge/vis/material/configEditor.kt new file mode 100644 index 00000000..821a073f --- /dev/null +++ b/ui/material/src/main/kotlin/hep/dataforge/vis/material/configEditor.kt @@ -0,0 +1,193 @@ +package hep.dataforge.vis.material + +import hep.dataforge.meta.* +import hep.dataforge.meta.descriptors.* +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken +import hep.dataforge.names.isEmpty +import hep.dataforge.names.plus +import hep.dataforge.vis.react.component +import hep.dataforge.vis.react.state +import kotlinx.css.Display +import kotlinx.css.display +import kotlinx.css.flexGrow +import kotlinx.css.flexShrink +import kotlinx.html.js.onClickFunction +import materialui.components.button.button +import materialui.components.grid.enums.GridAlignItems +import materialui.components.grid.enums.GridJustify +import materialui.components.grid.grid +import materialui.components.typography.typographyH6 +import materialui.lab.components.treeItem.treeItem +import materialui.lab.components.treeView.treeView +import org.w3c.dom.Element +import org.w3c.dom.events.Event +import react.* +import react.dom.render +import react.dom.span +import styled.css +import styled.styledDiv + +interface ConfigEditorProps : RProps { + + /** + * Root config object - always non null + */ + var root: Config + + /** + * Full path to the displayed node in [root]. Could be empty + */ + var name: Name + + /** + * Root default + */ + var default: Meta? + + /** + * Root descriptor + */ + var descriptor: NodeDescriptor? +} + +private fun RBuilder.configEditorItem( + root: Config, + name: Name, + descriptor: NodeDescriptor?, + default: Meta? +) { + val item = root[name] + val descriptorItem: ItemDescriptor? = descriptor?.get(name) + val defaultItem = default?.get(name) + val actualItem: MetaItem? = item ?: defaultItem ?: descriptorItem?.defaultItem() + + val token = name.last()?.toString() ?: "Properties" + + val removeClick: (Event) -> Unit = { + root.remove(name) + } + + treeItem { + attrs { + nodeId = name.toString() + label { + row { + attrs { + alignItems = GridAlignItems.stretch + justify = GridJustify.spaceBetween + spacing(1) + } + grid { + typographyH6 { + +token + } + } + if (actualItem is MetaItem.ValueItem) { + styledDiv { + css { + display = Display.flex + flexGrow = 1.0 + } + valueChooser(root, name, actualItem.value, descriptorItem as? ValueDescriptor) + } + } + if (!name.isEmpty()) { + styledDiv { + css { + display = Display.flex + flexShrink = 1.0 + } + button { + +"\u00D7" + attrs { + if (item == null) { + disabled = true + } else { + onClickFunction = removeClick + } + } + } + } + } + } + } + } + if (actualItem is MetaItem.NodeItem) { + val keys = buildSet { + (descriptorItem as? NodeDescriptor)?.items?.keys?.forEach { + add(NameToken(it)) + } + item?.node?.items?.keys?.let { addAll(it) } + defaultItem?.node?.items?.keys?.let { addAll(it) } + } + + keys.forEach { token -> + configEditorItem(root, name + token, descriptor, default) + } + } + } +} + +val ConfigEditor: FunctionalComponent = component { props -> + var kostyl by state { false } + + fun update() { + kostyl = !kostyl + } + + useEffectWithCleanup(listOf(props.root)) { + props.root.onChange(this) { name, _, _ -> + if (name == props.name) { + update() + } + } + return@useEffectWithCleanup { props.root.removeListener(this) } + } + + treeView { + attrs { + defaultCollapseIcon { + span { + +"-" + } + //child(ExpandMoreIcon::class) {} + }//{} + defaultExpandIcon { + span { + +"+" + } + //child(ChevronRightIcon::class) {} + }//{} + set("disableSelection", true) + } + configEditorItem(props.root, props.name, props.descriptor, props.default) + } + +} + +fun RBuilder.configEditor( + config: Config, + name: Name = Name.EMPTY, + descriptor: NodeDescriptor? = null, + default: Meta? = null +) { + child(ConfigEditor) { + attrs { + this.root = config + this.name = name + this.descriptor = descriptor + this.default = default + } + } +} + +fun Element.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null) { + render(this) { + configEditor(config, Name.EMPTY, descriptor, default) + } +} + +fun RBuilder.configEditor(obj: Configurable, descriptor: NodeDescriptor? = obj.descriptor, default: Meta? = null) { + configEditor(obj.config, Name.EMPTY, descriptor ?: obj.descriptor, default) +} diff --git a/ui/material/src/main/kotlin/hep/dataforge/vis/material/icons.kt b/ui/material/src/main/kotlin/hep/dataforge/vis/material/icons.kt new file mode 100644 index 00000000..1025b8ba --- /dev/null +++ b/ui/material/src/main/kotlin/hep/dataforge/vis/material/icons.kt @@ -0,0 +1,16 @@ +package hep.dataforge.vis.material + +import react.Component +import react.RClass +import react.RProps +import react.RState + +//@JsModule("@material-ui/icons/ExpandMore") +//external class ExpandMoreIcon : Component{ +// override fun render(): dynamic +//} +// +//@JsModule("@material-ui/icons/ChevronRight") +//external class ChevronRightIcon : Component{ +// override fun render(): dynamic +//} \ No newline at end of file diff --git a/ui/material/src/main/kotlin/hep/dataforge/vis/material/misc.kt b/ui/material/src/main/kotlin/hep/dataforge/vis/material/misc.kt new file mode 100644 index 00000000..2967cae1 --- /dev/null +++ b/ui/material/src/main/kotlin/hep/dataforge/vis/material/misc.kt @@ -0,0 +1,98 @@ +package hep.dataforge.vis.material + +import hep.dataforge.vis.react.component +import hep.dataforge.vis.react.state +import kotlinx.html.DIV +import materialui.components.card.card +import materialui.components.cardcontent.cardContent +import materialui.components.cardheader.cardHeader +import materialui.components.container.container +import materialui.components.container.enums.ContainerMaxWidth +import materialui.components.expansionpanel.expansionPanel +import materialui.components.expansionpaneldetails.expansionPanelDetails +import materialui.components.expansionpanelsummary.expansionPanelSummary +import materialui.components.grid.GridElementBuilder +import materialui.components.grid.enums.GridDirection +import materialui.components.grid.enums.GridStyle +import materialui.components.grid.grid +import materialui.components.paper.paper +import materialui.components.typography.typographyH3 +import materialui.components.typography.typographyH5 +import react.RBuilder +import react.RProps +import react.child +import react.dom.RDOMBuilder + + +fun accordionComponent(elements: List.() -> Unit>>) = + component { + val expandedIndex: Int? by state { null } + + container { + attrs { + maxWidth = ContainerMaxWidth.`false` + } + elements.forEachIndexed { index, (header, body) -> + expansionPanel { + attrs { + expanded = index == expandedIndex + } + expansionPanelSummary { + typographyH5 { + +header + } + } + expansionPanelDetails { + this.body() + } + } + } + } + } + +typealias RAccordionBuilder = MutableList.() -> Unit>> + +fun RAccordionBuilder.entry(title: String, builder: RDOMBuilder
.() -> Unit) { + add(title to builder) +} + +fun RBuilder.accordion(builder: RAccordionBuilder.() -> Unit) { + val list: List.() -> Unit>> = + ArrayList.() -> Unit>>().apply(builder) + child(accordionComponent(list)) +} + +fun RBuilder.materialCard(title: String, block: RBuilder.() -> Unit) { + card { + cardHeader { + attrs { + this.title = typographyH5 { + +title + } + } + } + cardContent { + paper { + this.block() + } + } + } +} + +fun RBuilder.column(vararg classMap: Pair, block: GridElementBuilder
.() -> Unit) = + grid(*classMap) { + attrs { + container = true + direction = GridDirection.column + } + block() + } + +fun RBuilder.row(vararg classMap: Pair, block: GridElementBuilder
.() -> Unit) = + grid(*classMap) { + attrs { + container = true + direction = GridDirection.row + } + block() + } diff --git a/ui/material/src/main/kotlin/hep/dataforge/vis/material/objectTree.kt b/ui/material/src/main/kotlin/hep/dataforge/vis/material/objectTree.kt new file mode 100644 index 00000000..976ae836 --- /dev/null +++ b/ui/material/src/main/kotlin/hep/dataforge/vis/material/objectTree.kt @@ -0,0 +1,92 @@ +package hep.dataforge.vis.material + +import hep.dataforge.names.Name +import hep.dataforge.names.plus +import hep.dataforge.names.toName +import hep.dataforge.vis.VisualGroup +import hep.dataforge.vis.VisualObject +import hep.dataforge.vis.isEmpty +import hep.dataforge.vis.react.component +import hep.dataforge.vis.react.state +import materialui.lab.components.treeItem.treeItem +import materialui.lab.components.treeView.treeView +import react.FunctionalComponent +import react.RBuilder +import react.RProps +import react.child +import react.dom.span + +interface ObjectTreeProps : RProps { + var name: Name + var selected: Name? + var obj: VisualObject + var clickCallback: (Name?) -> Unit +} + +private fun RBuilder.treeBranch(name: Name, obj: VisualObject): Unit { + treeItem { + val token = name.last()?.toString() ?: "World" + attrs { + nodeId = name.toString() + label { + span { + +token + } + } + } + + if (obj is VisualGroup) { + obj.children.entries + .filter { !it.key.toString().startsWith("@") } // ignore statics and other hidden children + .sortedBy { (it.value as? VisualGroup)?.isEmpty ?: true } + .forEach { (childToken, child) -> + treeBranch(name + childToken, child) + } + } + } +} + +val ObjectTree: FunctionalComponent = component { props -> + var selected: String? by state { props.selected.toString() } + treeView { + attrs { + this.selected = selected + this.onNodeSelect = { _, selectedItem -> + @Suppress("CAST_NEVER_SUCCEEDS") + selected = (selectedItem as? String) + val itemName = selected?.toName() + props.clickCallback(itemName) + Unit + } + defaultCollapseIcon { + span{ + +"-" + } + //child(ExpandMoreIcon::class) {} + }//{} + defaultExpandIcon { + span{ + +"+" + } + //child(ChevronRightIcon::class) {} + }//{} + } + treeBranch(props.name, props.obj) + } +} + +fun RBuilder.objectTree( + visualObject: VisualObject, + selected: Name? = null, + clickCallback: (Name?) -> Unit = {} +) { + child(ObjectTree) { + attrs { + this.name = Name.EMPTY + this.obj = visualObject + this.selected = selected + this.clickCallback = clickCallback + } + } +} + diff --git a/ui/material/src/main/kotlin/hep/dataforge/vis/material/valueChooser.kt b/ui/material/src/main/kotlin/hep/dataforge/vis/material/valueChooser.kt new file mode 100644 index 00000000..7ccf296d --- /dev/null +++ b/ui/material/src/main/kotlin/hep/dataforge/vis/material/valueChooser.kt @@ -0,0 +1,114 @@ +package hep.dataforge.vis.material + +import hep.dataforge.meta.Config +import hep.dataforge.meta.descriptors.ValueDescriptor +import hep.dataforge.meta.get +import hep.dataforge.meta.number +import hep.dataforge.meta.setValue +import hep.dataforge.names.Name +import hep.dataforge.values.* +import hep.dataforge.vis.widgetType +import kotlinx.html.InputType +import kotlinx.html.js.onChangeFunction +import kotlinx.html.js.onKeyDownFunction +import materialui.components.input.input +import materialui.components.select.select +import materialui.components.slider.slider +import materialui.components.switches.switch +import materialui.components.textfield.textField +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLSelectElement +import org.w3c.dom.events.Event +import org.w3c.dom.events.KeyboardEvent +import react.RBuilder +import react.dom.option + +internal fun RBuilder.valueChooser(root: Config, name: Name, value: Value, descriptor: ValueDescriptor?) { + val onValueChange: (Event) -> Unit = { event -> + if (event !is KeyboardEvent || event.key == "Enter") { + val res = when (val t = event.target) { + // (it.target as HTMLInputElement).value + is HTMLInputElement -> if (t.type == "checkbox") { + if (t.checked) True else False + } else { + t.value.asValue() + } + is HTMLSelectElement -> t.value.asValue() + else -> error("Unknown event target: $t") + } + + try { + root.setValue(name, res) + } catch (ex: Exception) { + console.error("Can't set config property ${name} to $res") + } + } + } + + + val type = descriptor?.type?.firstOrNull() + when { + descriptor?.widgetType == "slider" -> slider { + attrs { + descriptor.attributes["step"].number?.let { + step = it + } + descriptor.attributes["min"].number?.let { + min = it + } + descriptor.attributes["max"].number?.let { + max = it + } + this.defaultValue = value.number + onChangeFunction = onValueChange + } + } + descriptor?.widgetType == "color" -> input { + attrs { + fullWidth = true + this.type = InputType.color + this.value = value.string + onChangeFunction = onValueChange + } + } + + type == ValueType.BOOLEAN -> switch { + attrs { + defaultChecked = value.boolean + onChangeFunction = onValueChange + } + } + + type == ValueType.NUMBER -> textField { + attrs { + fullWidth = true + this.type = InputType.number + defaultValue = value.string + onChangeFunction = onValueChange + //onKeyDownFunction = onValueChange + } + } + descriptor?.allowedValues?.isNotEmpty() ?: false -> select { + descriptor!!.allowedValues.forEach { + option { + +it.string + } + } + attrs { + fullWidth = true + multiple = false + onChangeFunction = onValueChange + } + } + else -> textField { + attrs { + this.type = InputType.text + fullWidth = true + this.defaultValue = value.string + //onFocusOutFunction = onValueChange + onKeyDownFunction = onValueChange + } + } + } + +} \ No newline at end of file diff --git a/ui/material/src/main/kotlin/materialui/components/slider/builder.kt b/ui/material/src/main/kotlin/materialui/components/slider/builder.kt new file mode 100644 index 00000000..3e764c15 --- /dev/null +++ b/ui/material/src/main/kotlin/materialui/components/slider/builder.kt @@ -0,0 +1,37 @@ +package materialui.components.slider + +import kotlinx.html.DIV +import kotlinx.html.Tag +import materialui.components.MaterialElementBuilder +import materialui.components.getValue +import materialui.components.inputbase.enums.InputBaseStyle +import materialui.components.setValue +import react.RClass +import react.ReactElement + +class SliderElementBuilder internal constructor( + type: RClass, + classMap: List, String>> +) : MaterialElementBuilder(type, classMap, { DIV(mapOf(), it) }) { + fun Tag.classes(vararg classMap: Pair) { + classes(classMap.toList()) + } + var Tag.defaultValue: Number? by materialProps + var Tag.disabled: Boolean? by materialProps// = false + var Tag.getAriaLabel: String? by materialProps + var Tag.getAriaValueText: String? by materialProps + var Tag.marks: Array? by materialProps + var Tag.max: Number? by materialProps// = 100 + var Tag.min: Number? by materialProps// = 0, + var Tag.name: String? by materialProps + var Tag.onChange: ((dynamic, Number) -> Unit)? by materialProps + var Tag.onChangeCommitted: ((dynamic, Number) -> Unit)? by materialProps + var Tag.orientation: SliderOrientation? by materialProps + var Tag.scale: ((Number) -> Number)? by materialProps// {it} + var Tag.step: Number? by materialProps// = 1, + //ThumbComponent = 'span', + var Tag.track: SliderTrack by materialProps + var Tag.value: Number? by materialProps + var Tag.ValueLabelComponent: ReactElement? by materialProps + var Tag.valueLabelDisplay: SliderValueLabelDisplay by materialProps +} \ No newline at end of file diff --git a/ui/material/src/main/kotlin/materialui/components/slider/enums.kt b/ui/material/src/main/kotlin/materialui/components/slider/enums.kt new file mode 100644 index 00000000..8a8f9148 --- /dev/null +++ b/ui/material/src/main/kotlin/materialui/components/slider/enums.kt @@ -0,0 +1,18 @@ +package materialui.components.slider + +enum class SliderOrientation { + horizontal, + vertical +} + +enum class SliderTrack{ + normal, + `false`, + inverted, +} + +enum class SliderValueLabelDisplay{ + on, + auto, + off +} \ No newline at end of file diff --git a/ui/material/src/main/kotlin/materialui/components/slider/slider.kt b/ui/material/src/main/kotlin/materialui/components/slider/slider.kt new file mode 100644 index 00000000..12a8f8e0 --- /dev/null +++ b/ui/material/src/main/kotlin/materialui/components/slider/slider.kt @@ -0,0 +1,38 @@ +package materialui.components.slider + +import materialui.components.StandardProps +import materialui.components.input.enums.InputStyle +import react.RBuilder +import react.RClass +import react.ReactElement + +@JsModule("@material-ui/core/Slider") +private external val sliderModule: dynamic + +external interface SliderProps : StandardProps { + var defaultValue: Number? + var disabled: Boolean?// = false + var getAriaLabel: String? + var getAriaValueText: String? + var marks: Array? + var max: Number?// = 100 + var min: Number?// = 0, + var name: String? + var onChange: ((dynamic, Number) -> Unit)? + var onChangeCommitted: ((dynamic, Number) -> Unit)? + var orientation: SliderOrientation? + var scale: ((Number) -> Number)?// {it} + var step: Number? // = 1, + //ThumbComponent = 'span', + var track: SliderTrack + var value: Number? + var ValueLabelComponent: ReactElement? + var valueLabelDisplay: SliderValueLabelDisplay + //valueLabelFormat = Identity, +} + +@Suppress("UnsafeCastFromDynamic") +private val sliderComponent: RClass = sliderModule.default + +fun RBuilder.slider(vararg classMap: Pair, block: SliderElementBuilder.() -> Unit) = + child(SliderElementBuilder(sliderComponent, classMap.toList()).apply(block).create()) diff --git a/ui/react/build.gradle.kts b/ui/react/build.gradle.kts new file mode 100644 index 00000000..57aa27a6 --- /dev/null +++ b/ui/react/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("scientifik.js") +} + +kotlin { + target { + useCommonJs() + } +} + + +dependencies{ + + api("org.jetbrains:kotlin-react:16.13.1-pre.104-kotlin-1.3.72") + api("org.jetbrains:kotlin-react-dom:16.13.1-pre.104-kotlin-1.3.72") + api("org.jetbrains.kotlinx:kotlinx-html:0.6.12") + + api("org.jetbrains:kotlin-extensions:1.0.1-pre.104-kotlin-1.3.72") + api("org.jetbrains:kotlin-css-js:1.0.0-pre.94-kotlin-1.3.70") + api("org.jetbrains:kotlin-styled:1.0.0-pre.104-kotlin-1.3.72") + + api(npm("core-js", "2.6.5")) + + api(npm("react", "16.13.1")) + api(npm("react-dom", "16.13.1")) + + api(npm("react-is", "16.13.0")) + api(npm("inline-style-prefixer", "5.1.0")) + api(npm("styled-components", "4.3.2")) +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt b/ui/react/src/main/kotlin/hep/dataforge/vis/react/react.kt similarity index 96% rename from dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt rename to ui/react/src/main/kotlin/hep/dataforge/vis/react/react.kt index 1ed93464..489a30e7 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt +++ b/ui/react/src/main/kotlin/hep/dataforge/vis/react/react.kt @@ -1,4 +1,4 @@ -package hep.dataforge.js +package hep.dataforge.vis.react import react.* import kotlin.properties.ReadWriteProperty diff --git a/ui/ring/build.gradle.kts b/ui/ring/build.gradle.kts new file mode 100644 index 00000000..6cb807a3 --- /dev/null +++ b/ui/ring/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("scientifik.js") +} + +val dataforgeVersion: String by rootProject.extra + +kotlin { + target { + useCommonJs() + } +} + + +dependencies{ + api(project(":ui:react")) + + implementation(npm("@jetbrains/logos", "1.1.6")) + implementation(npm("@jetbrains/ring-ui", "3.0.13")) + + + implementation(npm("svg-inline-loader", "0.8.0")) +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/Alert.kt b/ui/ring/src/main/kotlin/ringui/Alert.kt new file mode 100644 index 00000000..10d9b5a7 --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/Alert.kt @@ -0,0 +1,34 @@ +package ringui + +import react.RBuilder +import react.RHandler +import react.dom.WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/alert/alert.js +external interface AlertProps : WithClassName { + var timeout: Number + var onCloseRequest: () -> Unit + var onClose: () -> Unit + var isShaking: Boolean + var isClosing: Boolean + var inline: Boolean + var showWithAnimation: Boolean + var closeable: Boolean + var type: AlertType +} + +typealias AlertType = String + +object AlertTypes { + var ERROR = "error" + var MESSAGE = "message" + var SUCCESS = "success" + var WARNING = "warning" + var LOADING = "loading" +} + +fun RBuilder.ringAlert(handler: RHandler) { + RingUI.Alert { + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/Button.kt b/ui/ring/src/main/kotlin/ringui/Button.kt new file mode 100644 index 00000000..cacbcad2 --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/Button.kt @@ -0,0 +1,35 @@ +package ringui + +import org.w3c.dom.events.MouseEvent +import react.RBuilder +import react.RHandler +import react.dom.WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/button/button.js +external interface ButtonProps : WithClassName { + var theme: String + var active: Boolean + var danger: Boolean + var delayed: Boolean + var loader: Boolean + var primary: Boolean + + var short: Boolean + var text: Boolean + var inline: Boolean + var dropdown: Boolean + + var href: String + + var icon: dynamic /* string | func */ + var iconSize: Number + var iconClassName: String + + var onMouseDown: (MouseEvent) -> Unit +} + +fun RBuilder.ringButton(handler: RHandler) { + RingUI.Button { + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/Dialog.kt b/ui/ring/src/main/kotlin/ringui/Dialog.kt new file mode 100644 index 00000000..6ef54bf5 --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/Dialog.kt @@ -0,0 +1,28 @@ +package ringui + +import react.RBuilder +import react.RHandler +import react.dom.WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/dialog/dialog.js +external interface DialogProps : WithClassName { + var contentClassName: String + var show: Boolean + var showCloseButton: Boolean + var onOverlayClick: () -> Unit + var onEscPress: () -> Unit + var onCloseClick: () -> Unit + // onCloseAttempt is a common callback for ESC pressing and overlay clicking. + // Use it if you don't need different behaviors for this cases. + var onCloseAttempt: () -> Unit + // focusTrap may break popups inside dialog, so use it carefully + var trapFocus: Boolean + var autoFocusFirst: Boolean +} + +fun RBuilder.ringDialog(show: Boolean, handler: RHandler) { + RingUI.Dialog { + attrs.show = show + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/Icon.kt b/ui/ring/src/main/kotlin/ringui/Icon.kt new file mode 100644 index 00000000..3bb9c640 --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/Icon.kt @@ -0,0 +1,21 @@ +package ringui + +import react.RBuilder +import react.RHandler +import react.dom.WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/icon/icon.js +external interface IconProps : WithClassName { + var color: String + var glyph: dynamic /* string | func */ + var height: Number + var size: Number + var width: Number + var loading: Boolean +} + +fun RBuilder.ringIcon(handler: RHandler) { + RingUI.Icon { + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/Link.kt b/ui/ring/src/main/kotlin/ringui/Link.kt new file mode 100644 index 00000000..bd3d7835 --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/Link.kt @@ -0,0 +1,24 @@ +package ringui + +import org.w3c.dom.events.MouseEvent +import react.RBuilder +import react.RHandler +import react.dom.WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/link/link.js +external interface LinkProps : WithClassName { + var innerClassName: String + var active: Boolean + var inherit: Boolean + var pseudo: Boolean + var hover: Boolean + var href: String + var onPlainLeftClick: (MouseEvent) -> Unit + var onClick: (MouseEvent) -> Unit +} + +fun RBuilder.ringLink(handler: RHandler) { + RingUI.Link { + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/RingUI.kt b/ui/ring/src/main/kotlin/ringui/RingUI.kt new file mode 100644 index 00000000..c7d3b5ef --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/RingUI.kt @@ -0,0 +1,14 @@ +package ringui + +import ringui.header.HeaderProps +import react.RClass + +@JsModule("@jetbrains/ring-ui") +external object RingUI { + val Alert: RClass + val Button: RClass + val Dialog: RClass + val Header: RClass + val Link: RClass + val Icon: RClass +} diff --git a/ui/ring/src/main/kotlin/ringui/UserCard.kt b/ui/ring/src/main/kotlin/ringui/UserCard.kt new file mode 100644 index 00000000..97f5192f --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/UserCard.kt @@ -0,0 +1,38 @@ +package ringui + +import react.RBuilder +import react.RClass +import react.RHandler +import react.RProps + +@JsModule("@jetbrains/ring-ui/components/user-card/user-card") +private external object UserCardModule { + val UserCard: RClass +} + +// https://github.com/JetBrains/ring-ui/blob/master/components/user-card/card.js +external interface UserCardProps : RProps { + var user: UserCardModel + var wording: UserCardWording +} + +data class UserCardModel( + val name: String, + val login: String, + val avatarUrl: String, + val email: String? = null, + val href: String? = null +) + +data class UserCardWording( + val banned: String, + val online: String, + val offline: String +) + +fun RBuilder.ringUserCard(user: UserCardModel, handler: RHandler = {}) { + UserCardModule.UserCard { + attrs.user = user + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/header/Header.kt b/ui/ring/src/main/kotlin/ringui/header/Header.kt new file mode 100644 index 00000000..e04635be --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/header/Header.kt @@ -0,0 +1,31 @@ +package ringui.header + +import ringui.RingUI +import react.RBuilder +import react.RClass +import react.RHandler +import react.dom.WithClassName + +@JsModule("@jetbrains/ring-ui/components/header/header") +internal external object HeaderModule { + val RerenderableHeader: RClass + val Logo: RClass + val Tray: RClass + val TrayIcon: RClass + val Profile: RClass + val SmartProfile: RClass + val Services: RClass + val SmartServices: RClass +} + +// https://github.com/JetBrains/ring-ui/blob/master/components/header/header.js +external interface HeaderProps : WithClassName { + var spaced: Boolean + var theme: String +} + +fun RBuilder.ringHeader(handler: RHandler) { + RingUI.Header { + handler() + } +} diff --git a/ui/ring/src/main/kotlin/ringui/header/Logo.kt b/ui/ring/src/main/kotlin/ringui/header/Logo.kt new file mode 100644 index 00000000..4a6b4472 --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/header/Logo.kt @@ -0,0 +1,21 @@ +package ringui.header + +import ringui.IconProps +import kotlinx.html.A +import react.RElementBuilder +import react.RHandler +import styled.StyledDOMBuilder + +external interface HeaderLogoProps : IconProps + +fun StyledDOMBuilder.ringLogo(handler: RHandler) { + HeaderModule.Logo { + handler() + } +} + +fun RElementBuilder.ringLogo(handler: RHandler) { + HeaderModule.Logo { + handler() + } +} diff --git a/ui/ring/src/main/kotlin/ringui/header/Tray.kt b/ui/ring/src/main/kotlin/ringui/header/Tray.kt new file mode 100644 index 00000000..ce47a44d --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/header/Tray.kt @@ -0,0 +1,26 @@ +package ringui.header + +import ringui.ButtonProps +import react.RElementBuilder +import react.RHandler +import react.dom.WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/header/tray.js +external interface HeaderTrayProps : WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/header/tray-icon.js +external interface HeaderTrayIconProps : ButtonProps { + var rotatable: Boolean +} + +fun RElementBuilder.ringTray(handler: RHandler) { + HeaderModule.Tray { + handler() + } +} + +fun RElementBuilder.ringTrayIcon(handler: RHandler) { + HeaderModule.TrayIcon { + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/island/Content.kt b/ui/ring/src/main/kotlin/ringui/island/Content.kt new file mode 100644 index 00000000..d183f702 --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/island/Content.kt @@ -0,0 +1,20 @@ +package ringui.island + +import react.RElementBuilder +import react.RHandler +import react.dom.WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/island/content.js +external interface IslandContentProps : WithClassName { + var scrollableWrapperClassName: String + var fade: Boolean + var bottomBorder: Boolean + var onScroll: () -> Unit + var onScrollToBottom: () -> Unit +} + +fun RElementBuilder.ringIslandContent(handler: RHandler) { + IslandModule.Content { + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/island/Header.kt b/ui/ring/src/main/kotlin/ringui/island/Header.kt new file mode 100644 index 00000000..952a3cfc --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/island/Header.kt @@ -0,0 +1,18 @@ +package ringui.island + +import react.RElementBuilder +import react.RHandler +import react.dom.WithClassName + +// https://github.com/JetBrains/ring-ui/blob/master/components/island/header.js +external interface IslandHeaderProps : WithClassName { + var border: Boolean + var wrapWithTitle: Boolean + var phase: Number +} + +fun RElementBuilder.ringIslandHeader(handler: RHandler) { + IslandModule.Header { + handler() + } +} \ No newline at end of file diff --git a/ui/ring/src/main/kotlin/ringui/island/Island.kt b/ui/ring/src/main/kotlin/ringui/island/Island.kt new file mode 100644 index 00000000..1e02e892 --- /dev/null +++ b/ui/ring/src/main/kotlin/ringui/island/Island.kt @@ -0,0 +1,32 @@ +package ringui.island + +import react.RBuilder +import react.RClass +import react.RHandler +import react.dom.WithClassName + +@JsModule("@jetbrains/ring-ui/components/island/island") +internal external object IslandModule { + val default: RClass + val Content: RClass + val Header: RClass + val AdaptiveIsland: RClass +} + +// https://github.com/JetBrains/ring-ui/blob/master/components/island/island.js +external interface IslandProps : WithClassName { + val narrow: Boolean + val withoutPaddings: Boolean +} + +fun RBuilder.ringIsland(handler: RHandler) { + IslandModule.default { + handler() + } +} + +fun RBuilder.ringAdaptiveIsland(handler: RHandler) { + IslandModule.AdaptiveIsland { + handler() + } +} \ No newline at end of file diff --git a/ui/ring/webpack.config.d/01.ring.js b/ui/ring/webpack.config.d/01.ring.js new file mode 100644 index 00000000..549b6c50 --- /dev/null +++ b/ui/ring/webpack.config.d/01.ring.js @@ -0,0 +1,7 @@ +// wrap is useful, because declaring variables in module can be already declared +// module creates own lexical environment +;(function (config) { + const ringConfig = require('@jetbrains/ring-ui/webpack.config').config; + + config.module.rules.push(...ringConfig.module.rules); +})(config); \ No newline at end of file diff --git a/ui/ring/webpack.config.d/02.svg.js b/ui/ring/webpack.config.d/02.svg.js new file mode 100644 index 00000000..df178fd3 --- /dev/null +++ b/ui/ring/webpack.config.d/02.svg.js @@ -0,0 +1,15 @@ +// wrap is useful, because declaring variables in module can be already declared +// module creates own lexical environment +;(function (config) { + const path = require("path"); + config.module.rules.push( + { + test: /\.svg$/, + loader: "svg-inline-loader", + options: {removeSVGTagAttrs: false}, + include: [ + path.resolve(require.resolve("@jetbrains/logos"), "..", "..") + ] + } + ); +})(config); \ No newline at end of file