mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-07 08:48:28 +08:00
Added TextArea, Menu and Select. Added some fields to InfoWidget and added the server select widget.
This commit is contained in:
parent
e6d6f292f4
commit
c028c09ac9
@ -7,7 +7,7 @@ import world.phantasmal.core.unsafeToNonNull
|
|||||||
/**
|
/**
|
||||||
* Starts observing its dependencies when the first observer on this val is registered. Stops
|
* Starts observing its dependencies when the first observer on this val is registered. Stops
|
||||||
* observing its dependencies when the last observer on this val is disposed. This way no extra
|
* observing its dependencies when the last observer on this val is disposed. This way no extra
|
||||||
* disposables need to be managed when e.g. [transform] is used.
|
* disposables need to be managed when e.g. [map] is used.
|
||||||
*/
|
*/
|
||||||
abstract class DependentVal<T>(
|
abstract class DependentVal<T>(
|
||||||
private val dependencies: Iterable<Val<*>>,
|
private val dependencies: Iterable<Val<*>>,
|
||||||
|
@ -4,7 +4,7 @@ import world.phantasmal.core.disposable.Disposable
|
|||||||
import world.phantasmal.core.disposable.disposable
|
import world.phantasmal.core.disposable.disposable
|
||||||
import world.phantasmal.core.unsafeToNonNull
|
import world.phantasmal.core.unsafeToNonNull
|
||||||
|
|
||||||
class FlatTransformedVal<T>(
|
class FlatMappedVal<T>(
|
||||||
dependencies: Iterable<Val<*>>,
|
dependencies: Iterable<Val<*>>,
|
||||||
private val compute: () -> Val<T>,
|
private val compute: () -> Val<T>,
|
||||||
) : DependentVal<T>(dependencies) {
|
) : DependentVal<T>(dependencies) {
|
@ -1,6 +1,6 @@
|
|||||||
package world.phantasmal.observable.value
|
package world.phantasmal.observable.value
|
||||||
|
|
||||||
class TransformedVal<T>(
|
class MappedVal<T>(
|
||||||
dependencies: Iterable<Val<*>>,
|
dependencies: Iterable<Val<*>>,
|
||||||
private val compute: () -> T,
|
private val compute: () -> T,
|
||||||
) : DependentVal<T>(dependencies) {
|
) : DependentVal<T>(dependencies) {
|
@ -17,12 +17,12 @@ interface Val<out T> : Observable<T> {
|
|||||||
*/
|
*/
|
||||||
fun observe(callNow: Boolean = false, observer: ValObserver<T>): Disposable
|
fun observe(callNow: Boolean = false, observer: ValObserver<T>): Disposable
|
||||||
|
|
||||||
fun <R> transform(transform: (T) -> R): Val<R> =
|
fun <R> map(transform: (T) -> R): Val<R> =
|
||||||
TransformedVal(listOf(this)) { transform(value) }
|
MappedVal(listOf(this)) { transform(value) }
|
||||||
|
|
||||||
fun <T2, R> transform(v2: Val<T2>, transform: (T, T2) -> R): Val<R> =
|
fun <T2, R> map(v2: Val<T2>, transform: (T, T2) -> R): Val<R> =
|
||||||
TransformedVal(listOf(this, v2)) { transform(value, v2.value) }
|
MappedVal(listOf(this, v2)) { transform(value, v2.value) }
|
||||||
|
|
||||||
fun <R> flatTransform(transform: (T) -> Val<R>): Val<R> =
|
fun <R> flatMap(transform: (T) -> Val<R>): Val<R> =
|
||||||
FlatTransformedVal(listOf(this)) { transform(value) }
|
FlatMappedVal(listOf(this)) { transform(value) }
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package world.phantasmal.observable.value
|
package world.phantasmal.observable.value
|
||||||
|
|
||||||
infix fun Val<Boolean>.and(other: Val<Boolean>): Val<Boolean> =
|
infix fun Val<Boolean>.and(other: Val<Boolean>): Val<Boolean> =
|
||||||
transform(other) { a, b -> a && b }
|
map(other) { a, b -> a && b }
|
||||||
|
|
||||||
infix fun Val<Boolean>.or(other: Val<Boolean>): Val<Boolean> =
|
infix fun Val<Boolean>.or(other: Val<Boolean>): Val<Boolean> =
|
||||||
transform(other) { a, b -> a || b }
|
map(other) { a, b -> a || b }
|
||||||
|
|
||||||
// Use != because of https://youtrack.jetbrains.com/issue/KT-31277.
|
// Use != because of https://youtrack.jetbrains.com/issue/KT-31277.
|
||||||
infix fun Val<Boolean>.xor(other: Val<Boolean>): Val<Boolean> =
|
infix fun Val<Boolean>.xor(other: Val<Boolean>): Val<Boolean> =
|
||||||
transform(other) { a, b -> a != b }
|
map(other) { a, b -> a != b }
|
||||||
|
|
||||||
operator fun Val<Boolean>.not(): Val<Boolean> = transform { !it }
|
operator fun Val<Boolean>.not(): Val<Boolean> = map { !it }
|
||||||
|
@ -3,7 +3,7 @@ package world.phantasmal.observable.value.list
|
|||||||
import world.phantasmal.core.disposable.Disposable
|
import world.phantasmal.core.disposable.Disposable
|
||||||
import world.phantasmal.observable.value.Val
|
import world.phantasmal.observable.value.Val
|
||||||
|
|
||||||
interface ListVal<E> : Val<List<E>>, List<E> {
|
interface ListVal<E> : Val<List<E>> {
|
||||||
val sizeVal: Val<Int>
|
val sizeVal: Val<Int>
|
||||||
|
|
||||||
fun observeList(observer: ListValObserver<E>): Disposable
|
fun observeList(observer: ListValObserver<E>): Disposable
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
package world.phantasmal.observable.value.list
|
package world.phantasmal.observable.value.list
|
||||||
|
|
||||||
import world.phantasmal.observable.value.MutableVal
|
import world.phantasmal.observable.value.MutableVal
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
interface MutableListVal<E> : ListVal<E>, MutableVal<List<E>>, MutableList<E> {
|
interface MutableListVal<E> : ListVal<E>, MutableVal<List<E>> {
|
||||||
override operator fun getValue(thisRef: Any?, property: KProperty<*>): MutableList<E> = this
|
fun set(index: Int, element: E): E
|
||||||
|
|
||||||
|
fun add(element: E)
|
||||||
|
|
||||||
|
fun add(index: Int, element: E)
|
||||||
|
|
||||||
|
fun removeAt(index: Int): E
|
||||||
|
|
||||||
|
fun replaceAll(elements: Sequence<E>)
|
||||||
|
|
||||||
|
fun clear()
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ class SimpleListVal<E>(
|
|||||||
* will be propagated via ElementChange events.
|
* will be propagated via ElementChange events.
|
||||||
*/
|
*/
|
||||||
private val extractObservables: ObservablesExtractor<E>? = null,
|
private val extractObservables: ObservablesExtractor<E>? = null,
|
||||||
) : AbstractMutableList<E>(), MutableListVal<E> {
|
) : MutableListVal<E> {
|
||||||
override var value: List<E> = elements
|
override var value: List<E> = elements
|
||||||
set(value) {
|
set(value) {
|
||||||
val removed = ArrayList(elements)
|
val removed = ArrayList(elements)
|
||||||
@ -34,8 +34,6 @@ class SimpleListVal<E>(
|
|||||||
|
|
||||||
override val sizeVal: Val<Int> = mutableSizeVal
|
override val sizeVal: Val<Int> = mutableSizeVal
|
||||||
|
|
||||||
override val size: Int by sizeVal
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal observers which observe observables related to this list's elements so that their
|
* Internal observers which observe observables related to this list's elements so that their
|
||||||
* changes can be propagated via ElementChange events.
|
* changes can be propagated via ElementChange events.
|
||||||
@ -52,14 +50,18 @@ class SimpleListVal<E>(
|
|||||||
*/
|
*/
|
||||||
private val observers = mutableListOf<ValObserver<List<E>>>()
|
private val observers = mutableListOf<ValObserver<List<E>>>()
|
||||||
|
|
||||||
override fun get(index: Int): E = elements[index]
|
|
||||||
|
|
||||||
override fun set(index: Int, element: E): E {
|
override fun set(index: Int, element: E): E {
|
||||||
val removed = elements.set(index, element)
|
val removed = elements.set(index, element)
|
||||||
finalizeUpdate(ListValChangeEvent.Change(index, listOf(removed), listOf(element)))
|
finalizeUpdate(ListValChangeEvent.Change(index, listOf(removed), listOf(element)))
|
||||||
return removed
|
return removed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun add(element: E) {
|
||||||
|
val index = elements.size
|
||||||
|
elements.add(element)
|
||||||
|
finalizeUpdate(ListValChangeEvent.Change(index, emptyList(), listOf(element)))
|
||||||
|
}
|
||||||
|
|
||||||
override fun add(index: Int, element: E) {
|
override fun add(index: Int, element: E) {
|
||||||
elements.add(index, element)
|
elements.add(index, element)
|
||||||
finalizeUpdate(ListValChangeEvent.Change(index, emptyList(), listOf(element)))
|
finalizeUpdate(ListValChangeEvent.Change(index, emptyList(), listOf(element)))
|
||||||
@ -71,6 +73,19 @@ class SimpleListVal<E>(
|
|||||||
return removed
|
return removed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun replaceAll(elements: Sequence<E>) {
|
||||||
|
val removed = ArrayList(this.elements)
|
||||||
|
this.elements.clear()
|
||||||
|
this.elements.addAll(elements)
|
||||||
|
finalizeUpdate(ListValChangeEvent.Change(0, removed, this.elements))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
val removed = ArrayList(elements)
|
||||||
|
elements.clear()
|
||||||
|
finalizeUpdate(ListValChangeEvent.Change(0, removed, emptyList()))
|
||||||
|
}
|
||||||
|
|
||||||
override fun observe(observer: Observer<List<E>>): Disposable =
|
override fun observe(observer: Observer<List<E>>): Disposable =
|
||||||
observe(callNow = false, observer)
|
observe(callNow = false, observer)
|
||||||
|
|
||||||
|
@ -5,9 +5,9 @@ import kotlin.test.assertEquals
|
|||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In these tests the direct dependency of the [FlatTransformedVal] changes.
|
* In these tests the direct dependency of the [FlatMappedVal] changes.
|
||||||
*/
|
*/
|
||||||
class FlatTransformedValDependentValEmitsTests : RegularValTests() {
|
class FlatMappedValDependentValEmitsTests : RegularValTests() {
|
||||||
/**
|
/**
|
||||||
* This is a regression test, it's important that this exact sequence of statements stays the
|
* This is a regression test, it's important that this exact sequence of statements stays the
|
||||||
* same.
|
* same.
|
||||||
@ -15,7 +15,7 @@ class FlatTransformedValDependentValEmitsTests : RegularValTests() {
|
|||||||
@Test
|
@Test
|
||||||
fun emits_a_change_when_its_direct_val_dependency_changes() = test {
|
fun emits_a_change_when_its_direct_val_dependency_changes() = test {
|
||||||
val v = SimpleVal(SimpleVal(7))
|
val v = SimpleVal(SimpleVal(7))
|
||||||
val fv = FlatTransformedVal(listOf(v)) { v.value }
|
val fv = FlatMappedVal(listOf(v)) { v.value }
|
||||||
var observedValue: Int? = null
|
var observedValue: Int? = null
|
||||||
|
|
||||||
disposer.add(
|
disposer.add(
|
||||||
@ -35,13 +35,13 @@ class FlatTransformedValDependentValEmitsTests : RegularValTests() {
|
|||||||
|
|
||||||
override fun create(): ValAndEmit<*> {
|
override fun create(): ValAndEmit<*> {
|
||||||
val v = SimpleVal(SimpleVal(5))
|
val v = SimpleVal(SimpleVal(5))
|
||||||
val value = FlatTransformedVal(listOf(v)) { v.value }
|
val value = FlatMappedVal(listOf(v)) { v.value }
|
||||||
return ValAndEmit(value) { v.value = SimpleVal(v.value.value + 5) }
|
return ValAndEmit(value) { v.value = SimpleVal(v.value.value + 5) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
||||||
val v = SimpleVal(SimpleVal(bool))
|
val v = SimpleVal(SimpleVal(bool))
|
||||||
val value = FlatTransformedVal(listOf(v)) { v.value }
|
val value = FlatMappedVal(listOf(v)) { v.value }
|
||||||
return ValAndEmit(value) { v.value = SimpleVal(!v.value.value) }
|
return ValAndEmit(value) { v.value = SimpleVal(!v.value.value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,18 +1,18 @@
|
|||||||
package world.phantasmal.observable.value
|
package world.phantasmal.observable.value
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In these tests the dependency of the [FlatTransformedVal]'s direct dependency changes.
|
* In these tests the dependency of the [FlatMappedVal]'s direct dependency changes.
|
||||||
*/
|
*/
|
||||||
class FlatTransformedValNestedValEmitsTests : RegularValTests() {
|
class FlatMappedValNestedValEmitsTests : RegularValTests() {
|
||||||
override fun create(): ValAndEmit<*> {
|
override fun create(): ValAndEmit<*> {
|
||||||
val v = SimpleVal(SimpleVal(5))
|
val v = SimpleVal(SimpleVal(5))
|
||||||
val value = FlatTransformedVal(listOf(v)) { v.value }
|
val value = FlatMappedVal(listOf(v)) { v.value }
|
||||||
return ValAndEmit(value) { v.value.value += 5 }
|
return ValAndEmit(value) { v.value.value += 5 }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
||||||
val v = SimpleVal(SimpleVal(bool))
|
val v = SimpleVal(SimpleVal(bool))
|
||||||
val value = FlatTransformedVal(listOf(v)) { v.value }
|
val value = FlatMappedVal(listOf(v)) { v.value }
|
||||||
return ValAndEmit(value) { v.value.value = !v.value.value }
|
return ValAndEmit(value) { v.value.value = !v.value.value }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,15 +1,15 @@
|
|||||||
package world.phantasmal.observable.value
|
package world.phantasmal.observable.value
|
||||||
|
|
||||||
class TransformedValTests : RegularValTests() {
|
class MappedValTests : RegularValTests() {
|
||||||
override fun create(): ValAndEmit<*> {
|
override fun create(): ValAndEmit<*> {
|
||||||
val v = SimpleVal(0)
|
val v = SimpleVal(0)
|
||||||
val value = TransformedVal(listOf(v)) { 2 * v.value }
|
val value = MappedVal(listOf(v)) { 2 * v.value }
|
||||||
return ValAndEmit(value) { v.value += 2 }
|
return ValAndEmit(value) { v.value += 2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
override fun createBoolean(bool: Boolean): ValAndEmit<Boolean> {
|
||||||
val v = SimpleVal(bool)
|
val v = SimpleVal(bool)
|
||||||
val value = TransformedVal(listOf(v)) { v.value }
|
val value = MappedVal(listOf(v)) { v.value }
|
||||||
return ValAndEmit(value) { v.value = !v.value }
|
return ValAndEmit(value) { v.value = !v.value }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,7 +15,7 @@ abstract class ListValTests : ValTests() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun listVal_updates_sizeVal_correctly() = test {
|
fun listVal_updates_sizeVal_correctly() = test {
|
||||||
val (list: List<*>, add) = create()
|
val (list: ListVal<*>, add) = create()
|
||||||
|
|
||||||
assertEquals(0, list.sizeVal.value)
|
assertEquals(0, list.sizeVal.value)
|
||||||
|
|
||||||
|
@ -2,12 +2,16 @@ package world.phantasmal.web.application.widgets
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
|
import world.phantasmal.observable.value.trueVal
|
||||||
import world.phantasmal.web.application.controllers.NavigationController
|
import world.phantasmal.web.application.controllers.NavigationController
|
||||||
import world.phantasmal.webui.dom.div
|
import world.phantasmal.webui.dom.div
|
||||||
|
import world.phantasmal.webui.widgets.Select
|
||||||
import world.phantasmal.webui.widgets.Widget
|
import world.phantasmal.webui.widgets.Widget
|
||||||
|
|
||||||
class NavigationWidget(scope: CoroutineScope, private val ctrl: NavigationController) :
|
class NavigationWidget(
|
||||||
Widget(scope) {
|
scope: CoroutineScope,
|
||||||
|
private val ctrl: NavigationController,
|
||||||
|
) : Widget(scope) {
|
||||||
override fun Node.createElement() =
|
override fun Node.createElement() =
|
||||||
div {
|
div {
|
||||||
className = "pw-application-navigation"
|
className = "pw-application-navigation"
|
||||||
@ -15,6 +19,24 @@ class NavigationWidget(scope: CoroutineScope, private val ctrl: NavigationContro
|
|||||||
ctrl.tools.forEach { (tool, active) ->
|
ctrl.tools.forEach { (tool, active) ->
|
||||||
addChild(PwToolButton(scope, tool, active) { ctrl.setCurrentTool(tool) })
|
addChild(PwToolButton(scope, tool, active) { ctrl.setCurrentTool(tool) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
className = "pw-application-navigation-spacer"
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
className = "pw-application-navigation-right"
|
||||||
|
|
||||||
|
val serverSelect = Select(
|
||||||
|
scope,
|
||||||
|
disabled = trueVal(),
|
||||||
|
label = "Server:",
|
||||||
|
items = listOf("Ephinea"),
|
||||||
|
selected = "Ephinea",
|
||||||
|
tooltip = "Only Ephinea is supported at the moment",
|
||||||
|
)
|
||||||
|
addWidget(serverSelect.label!!)
|
||||||
|
addChild(serverSelect)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -35,18 +57,13 @@ class NavigationWidget(scope: CoroutineScope, private val ctrl: NavigationContro
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pw-application-navigation-server {
|
.pw-application-navigation-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pw-application-navigation-server > * {
|
.pw-application-navigation-right > * {
|
||||||
margin: 0 2px;
|
margin: 1px 2px;
|
||||||
}
|
|
||||||
|
|
||||||
.pw-application-navigation-time {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pw-application-navigation-github {
|
.pw-application-navigation-github {
|
||||||
|
@ -81,7 +81,7 @@ class UiStore(scope: CoroutineScope, private val applicationUrl: ApplicationUrl)
|
|||||||
|
|
||||||
toolToActive = tools
|
toolToActive = tools
|
||||||
.map { tool ->
|
.map { tool ->
|
||||||
tool to currentTool.transform { it == tool }
|
tool to currentTool.map { it == tool }
|
||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import world.phantasmal.observable.value.Val
|
import world.phantasmal.observable.value.Val
|
||||||
import world.phantasmal.observable.value.falseVal
|
import world.phantasmal.observable.value.falseVal
|
||||||
import world.phantasmal.web.core.newJsObject
|
import world.phantasmal.webui.newJsObject
|
||||||
import world.phantasmal.web.externals.GoldenLayout
|
import world.phantasmal.web.externals.GoldenLayout
|
||||||
import world.phantasmal.webui.dom.div
|
import world.phantasmal.webui.dom.div
|
||||||
import world.phantasmal.webui.widgets.Widget
|
import world.phantasmal.webui.widgets.Widget
|
||||||
@ -84,7 +84,9 @@ class DockWidget(
|
|||||||
idToCreate.forEach { (id, create) ->
|
idToCreate.forEach { (id, create) ->
|
||||||
goldenLayout.registerComponent(id) { container: GoldenLayout.Container ->
|
goldenLayout.registerComponent(id) { container: GoldenLayout.Container ->
|
||||||
val node = container.getElement()[0] as Node
|
val node = container.getElement()[0] as Node
|
||||||
node.addChild(create(scope))
|
val widget = create(scope)
|
||||||
|
node.addChild(widget)
|
||||||
|
widget.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class HuntMethodModel(
|
|||||||
*/
|
*/
|
||||||
val userTime: Val<Duration?> = _userTime
|
val userTime: Val<Duration?> = _userTime
|
||||||
|
|
||||||
val time: Val<Duration> = userTime.transform { it ?: defaultTime }
|
val time: Val<Duration> = userTime.map { it ?: defaultTime }
|
||||||
|
|
||||||
fun setUserTime(userTime: Duration?): HuntMethodModel {
|
fun setUserTime(userTime: Duration?): HuntMethodModel {
|
||||||
_userTime.value = userTime
|
_userTime.value = userTime
|
||||||
|
@ -93,9 +93,7 @@ class HuntMethodStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(UiDispatcher) {
|
withContext(UiDispatcher) {
|
||||||
// TODO: Add more performant replaceAll method.
|
_methods.replaceAll(methods)
|
||||||
_methods.clear()
|
|
||||||
_methods.addAll(methods)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,14 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
|||||||
import world.phantasmal.webui.controllers.Controller
|
import world.phantasmal.webui.controllers.Controller
|
||||||
|
|
||||||
class QuestInfoController(scope: CoroutineScope, store: QuestEditorStore) : Controller(scope) {
|
class QuestInfoController(scope: CoroutineScope, store: QuestEditorStore) : Controller(scope) {
|
||||||
val unavailable = store.currentQuest.transform { it == null }
|
val unavailable: Val<Boolean> = store.currentQuest.map { it == null }
|
||||||
val episode: Val<String> = store.currentQuest.transform { it?.episode?.name ?: "" }
|
val disabled: Val<Boolean> = store.questEditingDisabled
|
||||||
val id: Val<Int> = store.currentQuest.flatTransform { it?.id ?: value(0) }
|
|
||||||
val name: Val<String> = store.currentQuest.flatTransform { it?.name ?: value("") }
|
val episode: Val<String> = store.currentQuest.map { it?.episode?.name ?: "" }
|
||||||
|
val id: Val<Int> = store.currentQuest.flatMap { it?.id ?: value(0) }
|
||||||
|
val name: Val<String> = store.currentQuest.flatMap { it?.name ?: value("") }
|
||||||
|
val shortDescription: Val<String> =
|
||||||
|
store.currentQuest.flatMap { it?.shortDescription ?: value("") }
|
||||||
|
val longDescription: Val<String> =
|
||||||
|
store.currentQuest.flatMap { it?.longDescription ?: value("") }
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package world.phantasmal.web.questEditor.rendering
|
package world.phantasmal.web.questEditor.rendering
|
||||||
|
|
||||||
import org.w3c.dom.HTMLCanvasElement
|
import org.w3c.dom.HTMLCanvasElement
|
||||||
import world.phantasmal.web.core.newJsObject
|
import world.phantasmal.webui.newJsObject
|
||||||
import world.phantasmal.web.core.rendering.Renderer
|
import world.phantasmal.web.core.rendering.Renderer
|
||||||
import world.phantasmal.web.externals.*
|
import world.phantasmal.web.externals.*
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
@ -11,6 +11,9 @@ class QuestEditorStore(scope: CoroutineScope) : Store(scope) {
|
|||||||
|
|
||||||
val currentQuest: Val<QuestModel?> = _currentQuest
|
val currentQuest: Val<QuestModel?> = _currentQuest
|
||||||
|
|
||||||
|
// TODO: Take into account whether we're debugging or not.
|
||||||
|
val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null }
|
||||||
|
|
||||||
fun setCurrentQuest(quest: QuestModel?) {
|
fun setCurrentQuest(quest: QuestModel?) {
|
||||||
_currentQuest.value = quest
|
_currentQuest.value = quest
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,14 @@ import world.phantasmal.web.core.widgets.UnavailableWidget
|
|||||||
import world.phantasmal.web.questEditor.controllers.QuestInfoController
|
import world.phantasmal.web.questEditor.controllers.QuestInfoController
|
||||||
import world.phantasmal.webui.dom.*
|
import world.phantasmal.webui.dom.*
|
||||||
import world.phantasmal.webui.widgets.IntInput
|
import world.phantasmal.webui.widgets.IntInput
|
||||||
|
import world.phantasmal.webui.widgets.TextArea
|
||||||
import world.phantasmal.webui.widgets.TextInput
|
import world.phantasmal.webui.widgets.TextInput
|
||||||
import world.phantasmal.webui.widgets.Widget
|
import world.phantasmal.webui.widgets.Widget
|
||||||
|
|
||||||
class QuestInfoWidget(
|
class QuestInfoWidget(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
private val ctrl: QuestInfoController,
|
private val ctrl: QuestInfoController,
|
||||||
) : Widget(scope) {
|
) : Widget(scope, disabled = ctrl.disabled) {
|
||||||
override fun Node.createElement() =
|
override fun Node.createElement() =
|
||||||
div {
|
div {
|
||||||
className = "pw-quest-editor-quest-info"
|
className = "pw-quest-editor-quest-info"
|
||||||
@ -31,9 +32,10 @@ class QuestInfoWidget(
|
|||||||
td {
|
td {
|
||||||
addChild(IntInput(
|
addChild(IntInput(
|
||||||
this@QuestInfoWidget.scope,
|
this@QuestInfoWidget.scope,
|
||||||
|
disabled = ctrl.disabled,
|
||||||
valueVal = ctrl.id,
|
valueVal = ctrl.id,
|
||||||
min = 0,
|
min = 0,
|
||||||
step = 1
|
step = 1,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,8 +44,49 @@ class QuestInfoWidget(
|
|||||||
td {
|
td {
|
||||||
addChild(TextInput(
|
addChild(TextInput(
|
||||||
this@QuestInfoWidget.scope,
|
this@QuestInfoWidget.scope,
|
||||||
|
disabled = ctrl.disabled,
|
||||||
valueVal = ctrl.name,
|
valueVal = ctrl.name,
|
||||||
maxLength = 32
|
maxLength = 32,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th {
|
||||||
|
colSpan = 2
|
||||||
|
textContent = "Short description:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
colSpan = 2
|
||||||
|
addChild(TextArea(
|
||||||
|
this@QuestInfoWidget.scope,
|
||||||
|
disabled = ctrl.disabled,
|
||||||
|
valueVal = ctrl.shortDescription,
|
||||||
|
maxLength = 128,
|
||||||
|
fontFamily = "\"Courier New\", monospace",
|
||||||
|
cols = 25,
|
||||||
|
rows = 5,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th {
|
||||||
|
colSpan = 2
|
||||||
|
textContent = "Long description:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
colSpan = 2
|
||||||
|
addChild(TextArea(
|
||||||
|
this@QuestInfoWidget.scope,
|
||||||
|
disabled = ctrl.disabled,
|
||||||
|
valueVal = ctrl.longDescription,
|
||||||
|
maxLength = 288,
|
||||||
|
fontFamily = "\"Courier New\", monospace",
|
||||||
|
cols = 25,
|
||||||
|
rows = 10,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package world.phantasmal.web.core
|
package world.phantasmal.webui
|
||||||
|
|
||||||
fun <T> newJsObject(block: T.() -> Unit): T =
|
fun <T> newJsObject(block: T.() -> Unit): T =
|
||||||
js("{}").unsafeCast<T>().apply(block)
|
js("{}").unsafeCast<T>().apply(block)
|
@ -51,6 +51,9 @@ fun Node.table(block: HTMLTableElement.() -> Unit = {}): HTMLTableElement =
|
|||||||
fun Node.td(block: HTMLTableCellElement.() -> Unit = {}): HTMLTableCellElement =
|
fun Node.td(block: HTMLTableCellElement.() -> Unit = {}): HTMLTableCellElement =
|
||||||
appendHtmlEl("TD", block)
|
appendHtmlEl("TD", block)
|
||||||
|
|
||||||
|
fun Node.textarea(block: HTMLTextAreaElement.() -> Unit = {}): HTMLTextAreaElement =
|
||||||
|
appendHtmlEl("TEXTAREA", block)
|
||||||
|
|
||||||
fun Node.th(block: HTMLTableCellElement.() -> Unit = {}): HTMLTableCellElement =
|
fun Node.th(block: HTMLTableCellElement.() -> Unit = {}): HTMLTableCellElement =
|
||||||
appendHtmlEl("TH", block)
|
appendHtmlEl("TH", block)
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package world.phantasmal.webui.widgets
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
|
import org.w3c.dom.events.KeyboardEvent
|
||||||
import org.w3c.dom.events.MouseEvent
|
import org.w3c.dom.events.MouseEvent
|
||||||
import world.phantasmal.observable.value.Val
|
import world.phantasmal.observable.value.Val
|
||||||
import world.phantasmal.observable.value.falseVal
|
import world.phantasmal.observable.value.falseVal
|
||||||
@ -14,12 +15,22 @@ open class Button(
|
|||||||
disabled: Val<Boolean> = falseVal(),
|
disabled: Val<Boolean> = falseVal(),
|
||||||
private val text: String? = null,
|
private val text: String? = null,
|
||||||
private val textVal: Val<String>? = null,
|
private val textVal: Val<String>? = null,
|
||||||
private val onclick: ((MouseEvent) -> Unit)? = null,
|
private val onMouseDown: ((MouseEvent) -> Unit)? = null,
|
||||||
|
private val onMouseUp: ((MouseEvent) -> Unit)? = null,
|
||||||
|
private val onClick: ((MouseEvent) -> Unit)? = null,
|
||||||
|
private val onKeyDown: ((KeyboardEvent) -> Unit)? = null,
|
||||||
|
private val onKeyUp: ((KeyboardEvent) -> Unit)? = null,
|
||||||
|
private val onKeyPress: ((KeyboardEvent) -> Unit)? = null,
|
||||||
) : Control(scope, hidden, disabled) {
|
) : Control(scope, hidden, disabled) {
|
||||||
override fun Node.createElement() =
|
override fun Node.createElement() =
|
||||||
button {
|
button {
|
||||||
className = "pw-button"
|
className = "pw-button"
|
||||||
onclick = this@Button.onclick
|
onmousedown = onMouseDown
|
||||||
|
onmouseup = onMouseUp
|
||||||
|
onclick = onClick
|
||||||
|
onkeydown = onKeyDown
|
||||||
|
onkeyup = onKeyUp
|
||||||
|
onkeypress = onKeyPress
|
||||||
|
|
||||||
span {
|
span {
|
||||||
className = "pw-button-inner"
|
className = "pw-button-inner"
|
||||||
|
@ -12,4 +12,5 @@ abstract class Control(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
hidden: Val<Boolean> = falseVal(),
|
hidden: Val<Boolean> = falseVal(),
|
||||||
disabled: Val<Boolean> = falseVal(),
|
disabled: Val<Boolean> = falseVal(),
|
||||||
) : Widget(scope, hidden, disabled)
|
tooltip: String? = null,
|
||||||
|
) : Widget(scope, hidden, disabled, tooltip)
|
||||||
|
@ -11,23 +11,25 @@ class DoubleInput(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
hidden: Val<Boolean> = falseVal(),
|
hidden: Val<Boolean> = falseVal(),
|
||||||
disabled: Val<Boolean> = falseVal(),
|
disabled: Val<Boolean> = falseVal(),
|
||||||
|
tooltip: String? = null,
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
labelVal: Val<String>? = null,
|
labelVal: Val<String>? = null,
|
||||||
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
||||||
value: Double? = null,
|
value: Double? = null,
|
||||||
valueVal: Val<Double>? = null,
|
valueVal: Val<Double>? = null,
|
||||||
setValue: ((Double) -> Unit)? = null,
|
onChange: (Double) -> Unit = {},
|
||||||
roundTo: Int = 2,
|
roundTo: Int = 2,
|
||||||
) : NumberInput<Double>(
|
) : NumberInput<Double>(
|
||||||
scope,
|
scope,
|
||||||
hidden,
|
hidden,
|
||||||
disabled,
|
disabled,
|
||||||
|
tooltip,
|
||||||
label,
|
label,
|
||||||
labelVal,
|
labelVal,
|
||||||
preferredLabelPosition,
|
preferredLabelPosition,
|
||||||
value,
|
value,
|
||||||
valueVal,
|
valueVal,
|
||||||
setValue,
|
onChange,
|
||||||
min = null,
|
min = null,
|
||||||
max = null,
|
max = null,
|
||||||
step = null,
|
step = null,
|
||||||
|
@ -11,6 +11,7 @@ abstract class Input<T>(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
hidden: Val<Boolean>,
|
hidden: Val<Boolean>,
|
||||||
disabled: Val<Boolean>,
|
disabled: Val<Boolean>,
|
||||||
|
tooltip: String?,
|
||||||
label: String?,
|
label: String?,
|
||||||
labelVal: Val<String>?,
|
labelVal: Val<String>?,
|
||||||
preferredLabelPosition: LabelPosition,
|
preferredLabelPosition: LabelPosition,
|
||||||
@ -19,7 +20,7 @@ abstract class Input<T>(
|
|||||||
private val inputType: String,
|
private val inputType: String,
|
||||||
private val value: T?,
|
private val value: T?,
|
||||||
private val valueVal: Val<T>?,
|
private val valueVal: Val<T>?,
|
||||||
private val setValue: ((T) -> Unit)?,
|
private val onChange: (T) -> Unit,
|
||||||
private val maxLength: Int?,
|
private val maxLength: Int?,
|
||||||
private val min: Int?,
|
private val min: Int?,
|
||||||
private val max: Int?,
|
private val max: Int?,
|
||||||
@ -28,6 +29,7 @@ abstract class Input<T>(
|
|||||||
scope,
|
scope,
|
||||||
hidden,
|
hidden,
|
||||||
disabled,
|
disabled,
|
||||||
|
tooltip,
|
||||||
label,
|
label,
|
||||||
labelVal,
|
labelVal,
|
||||||
preferredLabelPosition,
|
preferredLabelPosition,
|
||||||
@ -42,13 +44,11 @@ abstract class Input<T>(
|
|||||||
|
|
||||||
observe(this@Input.disabled) { disabled = it }
|
observe(this@Input.disabled) { disabled = it }
|
||||||
|
|
||||||
if (setValue != null) {
|
onchange = { onChange(getInputValue(this)) }
|
||||||
onchange = { setValue.invoke(getInputValue(this)) }
|
|
||||||
|
|
||||||
onkeydown = { e ->
|
onkeydown = { e ->
|
||||||
if (e.key == "Enter") {
|
if (e.key == "Enter") {
|
||||||
setValue.invoke(getInputValue(this))
|
onChange(getInputValue(this))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,12 +9,13 @@ class IntInput(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
hidden: Val<Boolean> = falseVal(),
|
hidden: Val<Boolean> = falseVal(),
|
||||||
disabled: Val<Boolean> = falseVal(),
|
disabled: Val<Boolean> = falseVal(),
|
||||||
|
tooltip: String? = null,
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
labelVal: Val<String>? = null,
|
labelVal: Val<String>? = null,
|
||||||
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
||||||
value: Int? = null,
|
value: Int? = null,
|
||||||
valueVal: Val<Int>? = null,
|
valueVal: Val<Int>? = null,
|
||||||
setValue: ((Int) -> Unit)? = null,
|
onChange: (Int) -> Unit = {},
|
||||||
min: Int? = null,
|
min: Int? = null,
|
||||||
max: Int? = null,
|
max: Int? = null,
|
||||||
step: Int? = null,
|
step: Int? = null,
|
||||||
@ -22,12 +23,13 @@ class IntInput(
|
|||||||
scope,
|
scope,
|
||||||
hidden,
|
hidden,
|
||||||
disabled,
|
disabled,
|
||||||
|
tooltip,
|
||||||
label,
|
label,
|
||||||
labelVal,
|
labelVal,
|
||||||
preferredLabelPosition,
|
preferredLabelPosition,
|
||||||
value,
|
value,
|
||||||
valueVal,
|
valueVal,
|
||||||
setValue,
|
onChange,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
step,
|
step,
|
||||||
|
@ -2,7 +2,6 @@ package world.phantasmal.webui.widgets
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import world.phantasmal.observable.value.Val
|
import world.phantasmal.observable.value.Val
|
||||||
import world.phantasmal.observable.value.falseVal
|
|
||||||
|
|
||||||
enum class LabelPosition {
|
enum class LabelPosition {
|
||||||
Before,
|
Before,
|
||||||
@ -11,12 +10,13 @@ enum class LabelPosition {
|
|||||||
|
|
||||||
abstract class LabelledControl(
|
abstract class LabelledControl(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
hidden: Val<Boolean> = falseVal(),
|
hidden: Val<Boolean>,
|
||||||
disabled: Val<Boolean> = falseVal(),
|
disabled: Val<Boolean>,
|
||||||
label: String? = null,
|
tooltip: String? = null,
|
||||||
labelVal: Val<String>? = null,
|
label: String?,
|
||||||
|
labelVal: Val<String>?,
|
||||||
val preferredLabelPosition: LabelPosition,
|
val preferredLabelPosition: LabelPosition,
|
||||||
) : Control(scope, hidden, disabled) {
|
) : Control(scope, hidden, disabled, tooltip) {
|
||||||
val label: Label? by lazy {
|
val label: Label? by lazy {
|
||||||
if (label == null && labelVal == null) {
|
if (label == null && labelVal == null) {
|
||||||
null
|
null
|
||||||
|
220
webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt
Normal file
220
webui/src/main/kotlin/world/phantasmal/webui/widgets/Menu.kt
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package world.phantasmal.webui.widgets
|
||||||
|
|
||||||
|
import kotlinx.browser.document
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.w3c.dom.*
|
||||||
|
import org.w3c.dom.events.Event
|
||||||
|
import org.w3c.dom.events.KeyboardEvent
|
||||||
|
import org.w3c.dom.events.MouseEvent
|
||||||
|
import world.phantasmal.observable.value.Val
|
||||||
|
import world.phantasmal.observable.value.falseVal
|
||||||
|
import world.phantasmal.observable.value.value
|
||||||
|
import world.phantasmal.webui.dom.disposableListener
|
||||||
|
import world.phantasmal.webui.dom.div
|
||||||
|
import world.phantasmal.webui.newJsObject
|
||||||
|
|
||||||
|
class Menu<T : Any>(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
hidden: Val<Boolean> = falseVal(),
|
||||||
|
disabled: Val<Boolean> = falseVal(),
|
||||||
|
tooltip: String? = null,
|
||||||
|
items: List<T>? = null,
|
||||||
|
itemsVal: Val<List<T>>? = null,
|
||||||
|
private val itemToString: (T) -> String = Any::toString,
|
||||||
|
private val onSelect: (T) -> Unit = {},
|
||||||
|
private val onCancel: () -> Unit = {},
|
||||||
|
) : Widget(
|
||||||
|
scope,
|
||||||
|
hidden,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
) {
|
||||||
|
private val items: Val<List<T>> = itemsVal ?: value(items ?: emptyList())
|
||||||
|
private lateinit var innerElement: HTMLElement
|
||||||
|
private var highlightedIndex: Int? = null
|
||||||
|
private var highlightedElement: Element? = null
|
||||||
|
private var previouslyFocusedElement: Element? = null
|
||||||
|
|
||||||
|
override fun Node.createElement() =
|
||||||
|
div {
|
||||||
|
className = "pw-menu"
|
||||||
|
tabIndex = -1
|
||||||
|
onmouseup = ::onMouseUp
|
||||||
|
onkeydown = ::onKeyDown
|
||||||
|
onblur = { onBlur() }
|
||||||
|
|
||||||
|
innerElement = div {
|
||||||
|
className = "pw-menu-inner"
|
||||||
|
onmouseover = ::innerMouseOver
|
||||||
|
|
||||||
|
bindChildrenTo(items) { item, index ->
|
||||||
|
div {
|
||||||
|
dataset["index"] = index.toString()
|
||||||
|
textContent = itemToString(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(this@Menu.hidden) {
|
||||||
|
if (it) {
|
||||||
|
document.removeEventListener("mousedown", ::onDocumentMouseDown)
|
||||||
|
clearHighlightItem()
|
||||||
|
|
||||||
|
(previouslyFocusedElement as HTMLElement?)?.focus()
|
||||||
|
} else {
|
||||||
|
document.addEventListener("mousedown", ::onDocumentMouseDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(disabled) {
|
||||||
|
if (it) {
|
||||||
|
clearHighlightItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disposableListener(document, "keydown", ::onDocumentKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun internalDispose() {
|
||||||
|
document.removeEventListener("mousedown", ::onDocumentMouseDown)
|
||||||
|
super.internalDispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun focus() {
|
||||||
|
previouslyFocusedElement = document.activeElement
|
||||||
|
super.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun highlightItem(item: T) {
|
||||||
|
val idx = items.value.indexOf(item)
|
||||||
|
|
||||||
|
if (idx != -1) {
|
||||||
|
highlightItemAt(idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMouseUp(e: MouseEvent) {
|
||||||
|
val target = e.target
|
||||||
|
|
||||||
|
if (target !is HTMLElement) return
|
||||||
|
|
||||||
|
target.dataset["index"]?.toIntOrNull()?.let(::selectItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onKeyDown(e: KeyboardEvent) {
|
||||||
|
when (e.key) {
|
||||||
|
"ArrowDown" -> {
|
||||||
|
e.preventDefault()
|
||||||
|
highlightItemAt(
|
||||||
|
when (val idx = highlightedIndex) {
|
||||||
|
null, items.value.lastIndex -> 0
|
||||||
|
else -> idx + 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"ArrowUp" -> {
|
||||||
|
e.preventDefault()
|
||||||
|
highlightItemAt(
|
||||||
|
when (val idx = highlightedIndex) {
|
||||||
|
null, 0 -> items.value.lastIndex
|
||||||
|
else -> idx - 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"Enter", " " -> {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
highlightedIndex?.let(::selectItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBlur() {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun innerMouseOver(e: MouseEvent) {
|
||||||
|
val target = e.target
|
||||||
|
|
||||||
|
if (target is HTMLElement) {
|
||||||
|
target.dataset["index"]?.toIntOrNull()?.let(::highlightItemAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDocumentMouseDown(e: Event) {
|
||||||
|
val target = e.target
|
||||||
|
|
||||||
|
if (target !is Node || !element.contains(target)) {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDocumentKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearHighlightItem() {
|
||||||
|
highlightedElement?.classList?.remove("pw-menu-highlighted")
|
||||||
|
highlightedIndex = null
|
||||||
|
highlightedElement = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun highlightItemAt(index: Int) {
|
||||||
|
highlightedElement?.classList?.remove("pw-menu-highlighted")
|
||||||
|
|
||||||
|
if (disabled.value) return
|
||||||
|
|
||||||
|
highlightedElement = innerElement.children.item(index)
|
||||||
|
|
||||||
|
highlightedElement?.let {
|
||||||
|
highlightedIndex = index
|
||||||
|
it.classList.add("pw-menu-highlighted")
|
||||||
|
it.scrollIntoView(newJsObject { block = "nearest" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectItem(index: Int) {
|
||||||
|
if (disabled.value) return
|
||||||
|
|
||||||
|
items.value.getOrNull(index)?.let(onSelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
@Suppress("CssUnusedSymbol", "CssUnresolvedCustomProperty")
|
||||||
|
// language=css
|
||||||
|
style("""
|
||||||
|
.pw-menu {
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
border: var(--pw-control-border);
|
||||||
|
--scrollbar-color: hsl(0, 0%, 18%);
|
||||||
|
--scrollbar-thumb-color: hsl(0, 0%, 22%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-menu > .pw-menu-inner {
|
||||||
|
overflow: auto;
|
||||||
|
background-color: var(--pw-control-bg-color);
|
||||||
|
max-height: 500px;
|
||||||
|
border: var(--pw-control-inner-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-menu > .pw-menu-inner > * {
|
||||||
|
padding: 4px 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-menu > .pw-menu-inner > .pw-menu-highlighted {
|
||||||
|
background-color: var(--pw-control-bg-color-hover);
|
||||||
|
color: var(--pw-control-text-color-hover);
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,12 +7,13 @@ abstract class NumberInput<T : Number>(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
hidden: Val<Boolean>,
|
hidden: Val<Boolean>,
|
||||||
disabled: Val<Boolean>,
|
disabled: Val<Boolean>,
|
||||||
|
tooltip: String?,
|
||||||
label: String?,
|
label: String?,
|
||||||
labelVal: Val<String>?,
|
labelVal: Val<String>?,
|
||||||
preferredLabelPosition: LabelPosition,
|
preferredLabelPosition: LabelPosition,
|
||||||
value: T?,
|
value: T?,
|
||||||
valueVal: Val<T>?,
|
valueVal: Val<T>?,
|
||||||
setValue: ((T) -> Unit)?,
|
onChange: (T) -> Unit,
|
||||||
min: Int?,
|
min: Int?,
|
||||||
max: Int?,
|
max: Int?,
|
||||||
step: Int?,
|
step: Int?,
|
||||||
@ -20,6 +21,7 @@ abstract class NumberInput<T : Number>(
|
|||||||
scope,
|
scope,
|
||||||
hidden,
|
hidden,
|
||||||
disabled,
|
disabled,
|
||||||
|
tooltip,
|
||||||
label,
|
label,
|
||||||
labelVal,
|
labelVal,
|
||||||
preferredLabelPosition,
|
preferredLabelPosition,
|
||||||
@ -28,7 +30,7 @@ abstract class NumberInput<T : Number>(
|
|||||||
inputType = "number",
|
inputType = "number",
|
||||||
value,
|
value,
|
||||||
valueVal,
|
valueVal,
|
||||||
setValue,
|
onChange,
|
||||||
maxLength = null,
|
maxLength = null,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
|
161
webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt
Normal file
161
webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package world.phantasmal.webui.widgets
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.w3c.dom.Node
|
||||||
|
import org.w3c.dom.events.KeyboardEvent
|
||||||
|
import org.w3c.dom.events.MouseEvent
|
||||||
|
import world.phantasmal.observable.value.Val
|
||||||
|
import world.phantasmal.observable.value.falseVal
|
||||||
|
import world.phantasmal.observable.value.mutableVal
|
||||||
|
import world.phantasmal.observable.value.value
|
||||||
|
import world.phantasmal.webui.dom.div
|
||||||
|
|
||||||
|
class Select<T : Any>(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
hidden: Val<Boolean> = falseVal(),
|
||||||
|
disabled: Val<Boolean> = falseVal(),
|
||||||
|
tooltip: String? = null,
|
||||||
|
label: String? = null,
|
||||||
|
labelVal: Val<String>? = null,
|
||||||
|
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
||||||
|
items: List<T>? = null,
|
||||||
|
itemsVal: Val<List<T>>? = null,
|
||||||
|
private val itemToString: (T) -> String = Any::toString,
|
||||||
|
selected: T? = null,
|
||||||
|
selectedVal: Val<T?>? = null,
|
||||||
|
private val onSelect: (T) -> Unit = {},
|
||||||
|
) : LabelledControl(
|
||||||
|
scope,
|
||||||
|
hidden,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
label,
|
||||||
|
labelVal,
|
||||||
|
preferredLabelPosition,
|
||||||
|
) {
|
||||||
|
private val items: Val<List<T>> = itemsVal ?: value(items ?: emptyList())
|
||||||
|
private val selected: Val<T?> = selectedVal ?: value(selected)
|
||||||
|
|
||||||
|
// Default to a single space so the inner text part won't be hidden.
|
||||||
|
private val buttonText = mutableVal(this.selected.value?.let(itemToString) ?: " ")
|
||||||
|
private val menuHidden = mutableVal(true)
|
||||||
|
|
||||||
|
private lateinit var menu: Menu<T>
|
||||||
|
private var justOpened = false
|
||||||
|
|
||||||
|
override fun Node.createElement() =
|
||||||
|
div {
|
||||||
|
className = "pw-select"
|
||||||
|
|
||||||
|
addWidget(Button(
|
||||||
|
scope,
|
||||||
|
disabled = disabled,
|
||||||
|
textVal = buttonText,
|
||||||
|
onMouseDown = ::onButtonMouseDown,
|
||||||
|
onMouseUp = { onButtonMouseUp() },
|
||||||
|
onKeyDown = ::onButtonKeyDown,
|
||||||
|
))
|
||||||
|
menu = addWidget(Menu(
|
||||||
|
scope,
|
||||||
|
hidden = menuHidden,
|
||||||
|
disabled = disabled,
|
||||||
|
itemsVal = items,
|
||||||
|
itemToString = itemToString,
|
||||||
|
onSelect = ::select,
|
||||||
|
onCancel = { menuHidden.value = true },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onButtonMouseDown(e: MouseEvent) {
|
||||||
|
e.stopPropagation()
|
||||||
|
justOpened = menuHidden.value
|
||||||
|
menuHidden.value = false
|
||||||
|
selected.value?.let(menu::highlightItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onButtonMouseUp() {
|
||||||
|
if (justOpened) {
|
||||||
|
menu.focus()
|
||||||
|
} else {
|
||||||
|
menuHidden.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
justOpened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onButtonKeyDown(e: KeyboardEvent) {
|
||||||
|
when (e.key) {
|
||||||
|
"Enter", " " -> {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
justOpened = menuHidden.value
|
||||||
|
menuHidden.value = false
|
||||||
|
selected.value?.let(menu::highlightItem)
|
||||||
|
menu.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
"ArrowUp" -> {
|
||||||
|
if (items.value.isNotEmpty()) {
|
||||||
|
if (selected.value == null) {
|
||||||
|
select(items.value.last())
|
||||||
|
} else {
|
||||||
|
val index = items.value.indexOf(selected.value) - 1
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
select(items.value.last())
|
||||||
|
} else {
|
||||||
|
select(items.value[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"ArrowDown" -> {
|
||||||
|
if (items.value.isNotEmpty()) {
|
||||||
|
if (selected.value == null) {
|
||||||
|
select(items.value.first())
|
||||||
|
} else {
|
||||||
|
val index = items.value.indexOf(selected.value) + 1
|
||||||
|
|
||||||
|
if (index >= items.value.size) {
|
||||||
|
select(items.value.first())
|
||||||
|
} else {
|
||||||
|
select(items.value[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun select(item: T) {
|
||||||
|
menuHidden.value = true
|
||||||
|
buttonText.value = itemToString(item)
|
||||||
|
onSelect(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
@Suppress("CssUnusedSymbol")
|
||||||
|
// language=css
|
||||||
|
style("""
|
||||||
|
.pw-select {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-select .pw-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-select .pw-menu {
|
||||||
|
top: 25px;
|
||||||
|
left: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -48,7 +48,7 @@ class TabContainer<T : Tab>(
|
|||||||
addChild(
|
addChild(
|
||||||
LazyLoader(
|
LazyLoader(
|
||||||
scope,
|
scope,
|
||||||
hidden = ctrl.activeTab.transform { it != tab },
|
hidden = ctrl.activeTab.map { it != tab },
|
||||||
createWidget = { scope -> createWidget(scope, tab) }
|
createWidget = { scope -> createWidget(scope, tab) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
102
webui/src/main/kotlin/world/phantasmal/webui/widgets/TextArea.kt
Normal file
102
webui/src/main/kotlin/world/phantasmal/webui/widgets/TextArea.kt
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package world.phantasmal.webui.widgets
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.w3c.dom.Node
|
||||||
|
import world.phantasmal.observable.value.Val
|
||||||
|
import world.phantasmal.observable.value.falseVal
|
||||||
|
import world.phantasmal.webui.dom.div
|
||||||
|
import world.phantasmal.webui.dom.textarea
|
||||||
|
|
||||||
|
class TextArea(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
hidden: Val<Boolean> = falseVal(),
|
||||||
|
disabled: Val<Boolean> = falseVal(),
|
||||||
|
tooltip: String? = null,
|
||||||
|
label: String? = null,
|
||||||
|
labelVal: Val<String>? = null,
|
||||||
|
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
||||||
|
private val value: String? = null,
|
||||||
|
private val valueVal: Val<String>? = null,
|
||||||
|
private val setValue: ((String) -> Unit)? = null,
|
||||||
|
private val maxLength: Int? = null,
|
||||||
|
private val fontFamily: String? = null,
|
||||||
|
private val rows: Int? = null,
|
||||||
|
private val cols: Int? = null,
|
||||||
|
) : LabelledControl(
|
||||||
|
scope,
|
||||||
|
hidden,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
label,
|
||||||
|
labelVal,
|
||||||
|
preferredLabelPosition,
|
||||||
|
) {
|
||||||
|
override fun Node.createElement() =
|
||||||
|
div {
|
||||||
|
className = "pw-text-area"
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
className = "pw-text-area-inner"
|
||||||
|
|
||||||
|
observe(this@TextArea.disabled) { disabled = it }
|
||||||
|
|
||||||
|
if (setValue != null) {
|
||||||
|
onchange = { setValue.invoke(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueVal != null) {
|
||||||
|
observe(valueVal) { value = it }
|
||||||
|
} else if (this@TextArea.value != null) {
|
||||||
|
value = this@TextArea.value
|
||||||
|
}
|
||||||
|
|
||||||
|
this@TextArea.maxLength?.let { maxLength = it }
|
||||||
|
fontFamily?.let { style.fontFamily = it }
|
||||||
|
this@TextArea.rows?.let { rows = it }
|
||||||
|
this@TextArea.cols?.let { cols = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
@Suppress("CssUnusedSymbol", "CssUnresolvedCustomProperty")
|
||||||
|
// language=css
|
||||||
|
style("""
|
||||||
|
.pw-text-area {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
border: var(--pw-input-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-text-area .pw-text-area-inner {
|
||||||
|
box-sizing: border-box;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 3px;
|
||||||
|
border: var(--pw-input-inner-border);
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--pw-input-bg-color);
|
||||||
|
color: var(--pw-input-text-color);
|
||||||
|
outline: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-text-area:hover {
|
||||||
|
border: var(--pw-input-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-text-area:focus-within {
|
||||||
|
border: var(--pw-input-border-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-text-area.disabled {
|
||||||
|
border: var(--pw-input-border-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-text-area.disabled .pw-text-area-inner {
|
||||||
|
color: var(--pw-input-text-color-disabled);
|
||||||
|
background-color: var(--pw-input-bg-color-disabled);
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,17 +9,19 @@ class TextInput(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
hidden: Val<Boolean> = falseVal(),
|
hidden: Val<Boolean> = falseVal(),
|
||||||
disabled: Val<Boolean> = falseVal(),
|
disabled: Val<Boolean> = falseVal(),
|
||||||
|
tooltip: String? = null,
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
labelVal: Val<String>? = null,
|
labelVal: Val<String>? = null,
|
||||||
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
preferredLabelPosition: LabelPosition = LabelPosition.Before,
|
||||||
value: String? = null,
|
value: String? = null,
|
||||||
valueVal: Val<String>? = null,
|
valueVal: Val<String>? = null,
|
||||||
setValue: ((String) -> Unit)? = null,
|
onChange: (String) -> Unit = {},
|
||||||
maxLength: Int? = null
|
maxLength: Int? = null,
|
||||||
) : Input<String>(
|
) : Input<String>(
|
||||||
scope,
|
scope,
|
||||||
hidden,
|
hidden,
|
||||||
disabled,
|
disabled,
|
||||||
|
tooltip,
|
||||||
label,
|
label,
|
||||||
labelVal,
|
labelVal,
|
||||||
preferredLabelPosition,
|
preferredLabelPosition,
|
||||||
@ -28,7 +30,7 @@ class TextInput(
|
|||||||
inputType = "text",
|
inputType = "text",
|
||||||
value,
|
value,
|
||||||
valueVal,
|
valueVal,
|
||||||
setValue,
|
onChange,
|
||||||
maxLength,
|
maxLength,
|
||||||
min = null,
|
min = null,
|
||||||
max = null,
|
max = null,
|
||||||
|
@ -2,7 +2,6 @@ package world.phantasmal.webui.widgets
|
|||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.dom.clear
|
|
||||||
import org.w3c.dom.*
|
import org.w3c.dom.*
|
||||||
import world.phantasmal.core.disposable.disposable
|
import world.phantasmal.core.disposable.disposable
|
||||||
import world.phantasmal.observable.Observable
|
import world.phantasmal.observable.Observable
|
||||||
@ -22,9 +21,10 @@ abstract class Widget(
|
|||||||
val hidden: Val<Boolean> = falseVal(),
|
val hidden: Val<Boolean> = falseVal(),
|
||||||
/**
|
/**
|
||||||
* By default determines the disabled attribute of its [element] and whether or not the
|
* By default determines the disabled attribute of its [element] and whether or not the
|
||||||
* `pw-disabled` class is added.
|
* "pw-disabled" class is added.
|
||||||
*/
|
*/
|
||||||
val disabled: Val<Boolean> = falseVal(),
|
val disabled: Val<Boolean> = falseVal(),
|
||||||
|
val tooltip: String? = null,
|
||||||
) : DisposableContainer() {
|
) : DisposableContainer() {
|
||||||
private val _ancestorHidden = mutableVal(false)
|
private val _ancestorHidden = mutableVal(false)
|
||||||
private val _children = mutableListOf<Widget>()
|
private val _children = mutableListOf<Widget>()
|
||||||
@ -49,6 +49,8 @@ abstract class Widget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tooltip?.let { el.title = it }
|
||||||
|
|
||||||
if (initResizeObserverRequested) {
|
if (initResizeObserverRequested) {
|
||||||
initResizeObserver(el)
|
initResizeObserver(el)
|
||||||
}
|
}
|
||||||
@ -74,6 +76,10 @@ abstract class Widget(
|
|||||||
|
|
||||||
val children: List<Widget> = _children
|
val children: List<Widget> = _children
|
||||||
|
|
||||||
|
open fun focus() {
|
||||||
|
element.focus()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called to initialize [element] when it is first accessed.
|
* Called to initialize [element] when it is first accessed.
|
||||||
*/
|
*/
|
||||||
@ -121,9 +127,30 @@ abstract class Widget(
|
|||||||
return child
|
return child
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun <T> Node.bindChildrenTo(
|
protected fun <T> Element.bindChildrenTo(
|
||||||
|
list: Val<List<T>>,
|
||||||
|
createChild: Node.(T, Int) -> Node,
|
||||||
|
) {
|
||||||
|
if (list is ListVal) {
|
||||||
|
bindChildrenTo(list, createChild)
|
||||||
|
} else {
|
||||||
|
observe(list) { items ->
|
||||||
|
innerHTML = ""
|
||||||
|
|
||||||
|
val frag = document.createDocumentFragment()
|
||||||
|
|
||||||
|
items.forEachIndexed { i, item ->
|
||||||
|
frag.createChild(item, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendChild(frag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun <T> Element.bindChildrenTo(
|
||||||
list: ListVal<T>,
|
list: ListVal<T>,
|
||||||
createChild: (T, Int) -> Node,
|
createChild: Node.(T, Int) -> Node,
|
||||||
) {
|
) {
|
||||||
fun spliceChildren(index: Int, removedCount: Int, inserted: List<T>) {
|
fun spliceChildren(index: Int, removedCount: Int, inserted: List<T>) {
|
||||||
for (i in 1..removedCount) {
|
for (i in 1..removedCount) {
|
||||||
@ -133,9 +160,7 @@ abstract class Widget(
|
|||||||
val frag = document.createDocumentFragment()
|
val frag = document.createDocumentFragment()
|
||||||
|
|
||||||
inserted.forEachIndexed { i, value ->
|
inserted.forEachIndexed { i, value ->
|
||||||
val child = createChild(value, index + i)
|
frag.createChild(value, index + i)
|
||||||
|
|
||||||
frag.append(child)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= childNodes.length) {
|
if (index >= childNodes.length) {
|
||||||
@ -145,25 +170,20 @@ abstract class Widget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val observer = list.observeList { change: ListValChangeEvent<T> ->
|
|
||||||
when (change) {
|
|
||||||
is ListValChangeEvent.Change -> {
|
|
||||||
spliceChildren(change.index, change.removed.size, change.inserted)
|
|
||||||
}
|
|
||||||
is ListValChangeEvent.ElementChange -> {
|
|
||||||
// TODO: Update children.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spliceChildren(0, 0, list.value)
|
|
||||||
|
|
||||||
addDisposable(
|
addDisposable(
|
||||||
disposable {
|
list.observeList { change: ListValChangeEvent<T> ->
|
||||||
observer.dispose()
|
when (change) {
|
||||||
clear()
|
is ListValChangeEvent.Change -> {
|
||||||
|
spliceChildren(change.index, change.removed.size, change.inserted)
|
||||||
|
}
|
||||||
|
is ListValChangeEvent.ElementChange -> {
|
||||||
|
// TODO: Update children.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
spliceChildren(0, 0, list.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user