Replace external bootsrap dependency with a copy
This commit is contained in:
parent
51bb46a45c
commit
fd5ff5e30c
@ -10,7 +10,7 @@ val dataforgeVersion by extra("0.8.0")
|
||||
|
||||
allprojects {
|
||||
group = "space.kscience"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2-dev-2"
|
||||
}
|
||||
|
||||
subprojects {
|
||||
@ -25,8 +25,8 @@ subprojects {
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xcontext-receivers")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@
|
||||
package space.kscience.visionforge.gdml.demo
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import app.softwork.bootstrapcompose.Container
|
||||
import app.softwork.bootstrapcompose.Icon
|
||||
import bootstrap.Container
|
||||
import bootstrap.Icon
|
||||
import org.jetbrains.compose.web.ExperimentalComposeWebApi
|
||||
import org.jetbrains.compose.web.attributes.InputType
|
||||
import org.jetbrains.compose.web.attributes.name
|
||||
|
@ -1,8 +1,8 @@
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import app.softwork.bootstrapcompose.Column
|
||||
import app.softwork.bootstrapcompose.Row
|
||||
import bootstrap.Column
|
||||
import bootstrap.Row
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import org.jetbrains.compose.web.css.*
|
||||
|
@ -1,65 +1,54 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
alias(spclibs.plugins.compose)
|
||||
// alias(spclibs.plugins.ktor)
|
||||
application
|
||||
}
|
||||
|
||||
group = "ru.mipt.npm"
|
||||
|
||||
val ktorVersion: String = spclibs.versions.ktor.get()
|
||||
|
||||
kscience {
|
||||
useCoroutines()
|
||||
useSerialization()
|
||||
useKtor()
|
||||
fullStack(
|
||||
"muon-monitor.js",
|
||||
jvmConfig = { withJava() },
|
||||
// jsConfig = { useCommonJs() },
|
||||
jvmConfig = {withJava()},
|
||||
browserConfig = {
|
||||
webpackTask{
|
||||
cssSupport{
|
||||
commonWebpackConfig {
|
||||
cssSupport {
|
||||
enabled = true
|
||||
}
|
||||
scssSupport{
|
||||
scssSupport {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
useCoroutines()
|
||||
useSerialization()
|
||||
useKtor()
|
||||
|
||||
commonMain {
|
||||
implementation(projects.visionforgeSolid)
|
||||
implementation(projects.visionforgeComposeHtml)
|
||||
}
|
||||
jvmMain {
|
||||
implementation("org.apache.commons:commons-math3:3.6.1")
|
||||
implementation("io.ktor:ktor-server-cio:${ktorVersion}")
|
||||
implementation("io.ktor:ktor-server-content-negotiation:${ktorVersion}")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:${ktorVersion}")
|
||||
implementation("ch.qos.logback:logback-classic:1.2.11")
|
||||
implementation("io.ktor:ktor-server-cio")
|
||||
implementation("io.ktor:ktor-server-content-negotiation")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json")
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
jsMain {
|
||||
implementation(projects.visionforgeThreejs)
|
||||
//implementation(devNpm("webpack-bundle-analyzer", "4.4.0"))
|
||||
}
|
||||
}
|
||||
kotlin{
|
||||
kotlin {
|
||||
explicitApi = null
|
||||
}
|
||||
|
||||
|
||||
application {
|
||||
mainClass.set("ru.mipt.npm.muon.monitor.server.MMServerKt")
|
||||
mainClass.set("ru.mipt.npm.muon.monitor.MMServerKt")
|
||||
}
|
||||
|
||||
//distributions {
|
||||
// main {
|
||||
// contents {
|
||||
// from("$buildDir/libs") {
|
||||
// rename("${rootProject.name}-jvm", rootProject.name)
|
||||
// into("lib")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
@ -3,16 +3,15 @@ package ru.mipt.npm.muon.monitor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import app.softwork.bootstrapcompose.Button
|
||||
import app.softwork.bootstrapcompose.ButtonGroup
|
||||
import app.softwork.bootstrapcompose.Color.Secondary
|
||||
import app.softwork.bootstrapcompose.Container
|
||||
import app.softwork.bootstrapcompose.Layout.Width
|
||||
import bootstrap.Button
|
||||
import bootstrap.ButtonGroup
|
||||
import bootstrap.Layout.Width.Full
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.await
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.compose.web.css.*
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.jetbrains.compose.web.dom.P
|
||||
import org.jetbrains.compose.web.dom.Span
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
@ -52,8 +51,9 @@ fun MMApp(context: Context, model: Model, selected: Name? = null) {
|
||||
|
||||
val events = remember { mutableStateListOf<Event>() }
|
||||
|
||||
Container(fluid = true,
|
||||
Div(
|
||||
attrs = {
|
||||
classes("container-fluid")
|
||||
style {
|
||||
height(100.vh - 12.pt)
|
||||
}
|
||||
@ -66,7 +66,7 @@ fun MMApp(context: Context, model: Model, selected: Name? = null) {
|
||||
options = mmOptions,
|
||||
sidebarTabs = {
|
||||
Tab("Events") {
|
||||
ButtonGroup({ Layout.width = Width.Full }) {
|
||||
ButtonGroup({ Layout.width = Full }) {
|
||||
Button("Next") {
|
||||
context.launch {
|
||||
val event = window.fetch(
|
||||
@ -85,7 +85,7 @@ fun MMApp(context: Context, model: Model, selected: Name? = null) {
|
||||
model.displayEvent(event)
|
||||
}
|
||||
}
|
||||
Button("Clear", color = Secondary) {
|
||||
Button("Clear", color = bootstrap.Color.Secondary) {
|
||||
events.clear()
|
||||
model.reset()
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.mipt.npm.muon.monitor.server
|
||||
package ru.mipt.npm.muon.monitor
|
||||
|
||||
|
||||
import io.ktor.http.ContentType
|
||||
@ -17,7 +17,6 @@ import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.get
|
||||
import org.apache.commons.math3.random.JDKRandomGenerator
|
||||
import ru.mipt.npm.muon.monitor.Model
|
||||
import ru.mipt.npm.muon.monitor.sim.Cos2TrackGenerator
|
||||
import ru.mipt.npm.muon.monitor.sim.simulateOne
|
||||
import space.kscience.dataforge.context.Context
|
||||
|
@ -12,7 +12,7 @@ repositories {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
|
||||
jvmToolchain(11)
|
||||
js(IR) {
|
||||
browser {
|
||||
webpackTask {
|
||||
@ -30,12 +30,9 @@ kotlin {
|
||||
|
||||
jvm {
|
||||
// withJava()
|
||||
compilations.all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs =
|
||||
freeCompilerArgs + "-Xjvm-default=all" + "-Xopt-in=kotlin.RequiresOptIn" + "-Xlambdas=indy" + "-Xcontext-receivers"
|
||||
}
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn", "-Xlambdas=indy", "-Xcontext-receivers")
|
||||
|
||||
}
|
||||
testRuns["test"].executionTask.configure {
|
||||
useJUnitPlatform()
|
||||
|
@ -1,12 +1,7 @@
|
||||
kotlin.code.style=official
|
||||
kotlin.mpp.stability.nowarn=true
|
||||
kotlin.js.compiler=ir
|
||||
|
||||
org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx4G
|
||||
|
||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||
|
||||
toolsVersion=0.15.2-kotlin-2.0.0-Beta5
|
||||
#kotlin.experimental.tryK2=true
|
||||
#kscience.wasm.disabled=true
|
||||
toolsVersion=0.15.2-kotlin-2.0.0-RC1
|
||||
|
@ -18,7 +18,6 @@ pluginManagement {
|
||||
id("space.kscience.gradle.project") version toolsVersion
|
||||
id("space.kscience.gradle.mpp") version toolsVersion
|
||||
id("space.kscience.gradle.jvm") version toolsVersion
|
||||
id("space.kscience.gradle.js") version toolsVersion
|
||||
}
|
||||
}
|
||||
|
||||
|
21
visionforge-compose-html/README.md
Normal file
21
visionforge-compose-html/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Module visionforge-compose-html
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:visionforge-compose-html:0.4.1`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
maven("https://repo.kotlin.link")
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:visionforge-compose-html:0.4.1")
|
||||
}
|
||||
```
|
@ -19,12 +19,11 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
jsMain{
|
||||
jsMain {
|
||||
dependencies {
|
||||
api("app.softwork:bootstrap-compose:0.1.15")
|
||||
api("app.softwork:bootstrap-compose-icons:0.1.15")
|
||||
implementation(compose.html.svg)
|
||||
|
||||
implementation(npm("bootstrap", "5.3.3"))
|
||||
implementation(npm(" bootstrap-icons", "1.11.3"))
|
||||
api("com.benasher44:uuid:0.8.4")
|
||||
api(compose.html.core)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.A
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Alert(
|
||||
color: Color,
|
||||
dismissible: Boolean = true,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
Div({
|
||||
classes("alert", "alert-$color")
|
||||
if (dismissible) {
|
||||
needsJS
|
||||
classes("alert-dismissible")
|
||||
}
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attr("role", "alert")
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun Link(
|
||||
href: String?,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLAnchorElement>? = null,
|
||||
content: ContentBuilder<HTMLAnchorElement>
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
A(href, {
|
||||
classes("alert-link")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}, content)
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.browser.window
|
||||
import org.jetbrains.compose.web.attributes.builders.InputAttrsScope
|
||||
import org.jetbrains.compose.web.css.*
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.HTMLButtonElement
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.EventTarget
|
||||
|
||||
private class ReferenceHolder<T>(var ref: T? = null)
|
||||
|
||||
/**
|
||||
* A TextInput widget with autocomplete/search suggestions that will display below the TextField. This component
|
||||
* only supports the "controlled" Input variant. This version provides limited support and expects the user to
|
||||
* listen to all input change events and build the suggestions list.
|
||||
*
|
||||
* @param inputAttrs attributes builder.
|
||||
* @param suggestions Composable to build the list of suggestions to display
|
||||
*/
|
||||
@Composable
|
||||
public fun Autocomplete(
|
||||
inputAttrs: InputAttrsScope<String>.() -> Unit = {},
|
||||
suggestions: ContentBuilder<HTMLDivElement>? = null
|
||||
) {
|
||||
var itemsVisible by remember { mutableStateOf(false) }
|
||||
val parentElement = remember { ReferenceHolder<Element>() }
|
||||
|
||||
DisposableEffect(true) {
|
||||
val effect: (Event) -> Unit = { event ->
|
||||
if (parentElement.ref?.isChild(event.target) != true) {
|
||||
itemsVisible = false
|
||||
}
|
||||
}
|
||||
window.document.addEventListener("mousedown", effect)
|
||||
onDispose {
|
||||
window.document.removeEventListener("mousedown", effect)
|
||||
}
|
||||
}
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
padding(0.px)
|
||||
}
|
||||
}
|
||||
) {
|
||||
DisposableEffect(true) {
|
||||
parentElement.ref = scopeElement
|
||||
onDispose { parentElement.ref = null }
|
||||
}
|
||||
|
||||
TextInput(attrs = {
|
||||
inputAttrs()
|
||||
onInput {
|
||||
itemsVisible = true
|
||||
}
|
||||
})
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
Style
|
||||
classes("dropdown-menu")
|
||||
if (itemsVisible) {
|
||||
classes("d-block")
|
||||
} else {
|
||||
classes("d-none")
|
||||
}
|
||||
onClick {
|
||||
itemsVisible = false
|
||||
}
|
||||
style {
|
||||
padding(0.px)
|
||||
}
|
||||
},
|
||||
content = suggestions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An AutoComplete that displays Strings as suggestions.
|
||||
*
|
||||
* @param value The value for the TextInput
|
||||
* @param onValueChange callback when the user changes the Input text
|
||||
* @param inputId The Id to assign to the TextInput
|
||||
* @param suggestions List of suggestions to be presented to the user
|
||||
* @param onSelection callback for when the user selects a suggestion. The TextInput value will not be updated until
|
||||
* this Composable is recomposed with an updated value.
|
||||
*/
|
||||
@Composable
|
||||
public fun Autocomplete(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
inputId: String? = null,
|
||||
suggestions: List<String> = listOf(),
|
||||
onSelection: (String) -> Unit
|
||||
) {
|
||||
Autocomplete(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
inputId = inputId,
|
||||
suggestions = suggestions,
|
||||
content = { _, item ->
|
||||
Text(item)
|
||||
},
|
||||
onSelection = { _, item -> onSelection(item) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A TextInput widget with autocomplete/search suggestions that will display below the TextField. This component
|
||||
* only supports the "controlled" Input variant. It uses a callback for displaying suggestion items.
|
||||
*
|
||||
* @param value The value for the TextInput
|
||||
* @param onValueChange callback when the user changes the Input text
|
||||
* @param inputId The Id to assign to the TextInput
|
||||
* @param suggestions List of suggestions to be presented to the user
|
||||
* @param content Composable for rendering item content. The functions first parameter is the items index and the 2nd
|
||||
* parameter is the item to be rendered.
|
||||
* @param onSelection callback for when the user selects a suggestion. The TextInput value will not be updated until
|
||||
* this Composable is recomposed with an updated value.
|
||||
*
|
||||
* This function would be better if using a generic type for the List items, content Composable callback parameter,
|
||||
* and onSelection callback, except 2 issues may be responsible for compiler failures:
|
||||
* https://github.com/JetBrains/compose-jb/issues/774
|
||||
* https://github.com/JetBrains/compose-jb/issues/1226
|
||||
*/
|
||||
@Composable
|
||||
public fun Autocomplete(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
inputId: String? = null,
|
||||
suggestions: List<String> = listOf(),
|
||||
content: @Composable ElementScope<HTMLButtonElement>.(index: Int, item: String) -> Unit,
|
||||
onSelection: (index: Int, item: String) -> Unit
|
||||
) {
|
||||
Autocomplete(inputAttrs = {
|
||||
inputId?.let { id(inputId) }
|
||||
value(value)
|
||||
onInput {
|
||||
onValueChange(it.value)
|
||||
}
|
||||
}) {
|
||||
Ul(attrs = {
|
||||
style {
|
||||
padding(0.px)
|
||||
margin(0.px)
|
||||
listStyle("none")
|
||||
}
|
||||
}) {
|
||||
suggestions.forEachIndexed { index, item ->
|
||||
Li(attrs = {
|
||||
style {
|
||||
padding(0.cssRem)
|
||||
}
|
||||
}) {
|
||||
Button(attrs = {
|
||||
classes("dropdown-item")
|
||||
onClick {
|
||||
onSelection(index, item)
|
||||
}
|
||||
}) {
|
||||
content(index, item)
|
||||
}
|
||||
if (index < suggestions.lastIndex) {
|
||||
Hr(attrs = {
|
||||
style {
|
||||
margin(0.px)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the element tree to see if this Element is a parent of the provided element.
|
||||
* @param element Element to check if it is a child of this Element
|
||||
*/
|
||||
private fun Element.isChild(element: Element): Boolean {
|
||||
var nextElement: Element? = element
|
||||
while (nextElement != this && nextElement != null) {
|
||||
nextElement = nextElement.parentElement
|
||||
}
|
||||
return nextElement == this
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses the element tree to see if this Element is a parent of the provided EventTarget.
|
||||
* @param eventTarget EventTarget to check if it is a child of this Element
|
||||
*/
|
||||
private fun Element.isChild(eventTarget: EventTarget?): Boolean {
|
||||
return if (eventTarget is Element) {
|
||||
isChild(eventTarget)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Span
|
||||
import org.w3c.dom.HTMLSpanElement
|
||||
|
||||
@Composable
|
||||
public fun Badge(
|
||||
backgroundColor: Color,
|
||||
textColor: Color? = null,
|
||||
round: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLSpanElement>? = null,
|
||||
content: ContentBuilder<HTMLSpanElement>
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Span(attrs = {
|
||||
classes("badge", backgroundColor.background())
|
||||
if (textColor != null) {
|
||||
classes(textColor.text())
|
||||
}
|
||||
if (round) {
|
||||
classes("rounded-pill")
|
||||
}
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}, content)
|
||||
}
|
26
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Box.kt
Normal file
26
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Box.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Box(
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}, content = content)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Brand(
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
classes("navbar-brand")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}, content)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package bootstrap
|
||||
|
||||
import org.jetbrains.compose.web.css.CSSLengthValue
|
||||
import org.jetbrains.compose.web.css.px
|
||||
|
||||
public enum class Breakpoint(private val classInfix: String) {
|
||||
Small("sm"),
|
||||
Medium("md"),
|
||||
Large("lg"),
|
||||
ExtraLarge("xl"),
|
||||
ExtraExtraLarge("xxl");
|
||||
|
||||
override fun toString(): String = classInfix
|
||||
}
|
||||
|
||||
/**
|
||||
* Breakpoint thresholds that match Bootstrap defaults. If the defaults were overridden in the
|
||||
* css (e.g. using sass to generate a custom Bootstrap css), then this variable should be updated with a
|
||||
* new map that matches before using classes/functions that create
|
||||
* new styles in a media query. If this is not done, the breakpoints won't be consistent with modified
|
||||
* bootstrap css classes.
|
||||
*/
|
||||
public var breakpoints: Map<Breakpoint, CSSLengthValue> = mapOf(
|
||||
Breakpoint.Small to 576.px,
|
||||
Breakpoint.Medium to 768.px,
|
||||
Breakpoint.Large to 992.px,
|
||||
Breakpoint.ExtraLarge to 1200.px,
|
||||
Breakpoint.ExtraExtraLarge to 1400.px,
|
||||
)
|
@ -0,0 +1,95 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.attributes.ButtonType
|
||||
import org.jetbrains.compose.web.attributes.disabled
|
||||
import org.jetbrains.compose.web.attributes.type
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.Button
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
import org.w3c.dom.HTMLButtonElement
|
||||
|
||||
@Composable
|
||||
public fun Button(
|
||||
title: String,
|
||||
color: Color = Color.Primary,
|
||||
size: ButtonSize = ButtonSize.Default,
|
||||
outlined: Boolean = false,
|
||||
type: ButtonType = ButtonType.Submit,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
|
||||
action: () -> Unit
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Button(attrs = {
|
||||
classes("btn")
|
||||
classes(size.toString())
|
||||
if (outlined) {
|
||||
classes("btn-outline-$color")
|
||||
} else {
|
||||
classes("btn-$color")
|
||||
}
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
type(type)
|
||||
onClick {
|
||||
action()
|
||||
}
|
||||
}) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
|
||||
public enum class ButtonSize(private val prefix: String) {
|
||||
Default("btn"), Large("btn-lg"), Small("btn-sm");
|
||||
|
||||
override fun toString(): String = prefix
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun Button(
|
||||
content: ContentBuilder<HTMLButtonElement>,
|
||||
color: Color = Color.Primary,
|
||||
outlined: Boolean = false,
|
||||
type: ButtonType = ButtonType.Submit,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
|
||||
action: () -> Unit
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Button(attrs = {
|
||||
classes("btn")
|
||||
if (outlined) {
|
||||
classes("btn-outline-$color")
|
||||
} else {
|
||||
classes("btn-$color")
|
||||
}
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
type(type)
|
||||
onClick {
|
||||
action()
|
||||
}
|
||||
}, content)
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun ButtonGroup(
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
content: ContentBuilder<HTMLDivElement>
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div({
|
||||
classes("btn-group")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
}, content)
|
||||
}
|
49
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Card.kt
Normal file
49
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Card.kt
Normal file
@ -0,0 +1,49 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Card(
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
headerAttrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
header: ContentBuilder<HTMLDivElement>? = null,
|
||||
footerAttrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
footer: ContentBuilder<HTMLDivElement>? = null,
|
||||
bodyAttrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
body: ContentBuilder<HTMLDivElement>
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div({
|
||||
classes("card")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
header?.let {
|
||||
Div({
|
||||
classes("card-header")
|
||||
headerAttrs?.invoke(this)
|
||||
}, header)
|
||||
}
|
||||
Div({
|
||||
classes("card-body")
|
||||
bodyAttrs?.invoke(this)
|
||||
}, body)
|
||||
footer?.let {
|
||||
Div({
|
||||
classes("card-footer")
|
||||
footerAttrs?.invoke(this)
|
||||
}, footer)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.InputType
|
||||
import org.jetbrains.compose.web.attributes.builders.InputAttrsScope
|
||||
import org.jetbrains.compose.web.attributes.disabled
|
||||
import org.jetbrains.compose.web.attributes.forId
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.jetbrains.compose.web.dom.Input
|
||||
import org.jetbrains.compose.web.dom.Label
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
|
||||
@Composable
|
||||
public fun Checkbox(
|
||||
checked: Boolean,
|
||||
label: String,
|
||||
id: String = remember { uuid4().toString() },
|
||||
disabled: Boolean = false,
|
||||
inline: Boolean = false,
|
||||
switch: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: (InputAttrsScope<Boolean>.() -> Unit)? = null,
|
||||
onClick: (Boolean) -> Unit
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div({
|
||||
classes(BSClasses.formCheck)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
if (inline) {
|
||||
classes(BSClasses.formCheckInline)
|
||||
}
|
||||
if (switch) {
|
||||
classes(BSClasses.formSwitch)
|
||||
}
|
||||
}) {
|
||||
Input(attrs = {
|
||||
classes(BSClasses.formCheckInput)
|
||||
id("_$id")
|
||||
checked(checked)
|
||||
onInput { event ->
|
||||
onClick(event.value)
|
||||
}
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}, type = InputType.Checkbox)
|
||||
Label(attrs = {
|
||||
classes(BSClasses.formCheckLabel)
|
||||
forId("_$id")
|
||||
}) {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package bootstrap
|
||||
|
||||
public object BSClasses {
|
||||
public const val disabled: String = "disabled"
|
||||
public const val active: String = "active"
|
||||
|
||||
public val align: Align = Align
|
||||
public val display: Display = Display
|
||||
|
||||
public const val colFormLabel: String = "col-form-label"
|
||||
public const val collapse: String = "collapse"
|
||||
public const val formCheck: String = "form-check"
|
||||
public const val formCheckInline: String = "form-check-inline"
|
||||
public const val formCheckInput: String = "form-check-input"
|
||||
public const val formCheckLabel: String = "form-check-label"
|
||||
public const val formControl: String = "form-control"
|
||||
public const val formFloating: String = "form-floating"
|
||||
public const val formLabel: String = "form-label"
|
||||
public const val formRange: String = "form-range"
|
||||
public const val formSelect: String = "form-select"
|
||||
public const val formSelectSmall: String = "form-select-sm"
|
||||
public const val formSelectLarge: String = "form-select-lg"
|
||||
public const val formSwitch: String = "form-switch"
|
||||
public const val formText: String = "form-text"
|
||||
public const val inputGroup: String = "input-group"
|
||||
public const val inputGroupLarge: String = "input-group-lg"
|
||||
public const val inputGroupSmall: String = "input-group-sm"
|
||||
public const val inputGroupText: String = "input-group-text"
|
||||
public const val listGroup: String = "list-group"
|
||||
public const val listGroupFlush: String = "list-group-flush"
|
||||
public const val listGroupHorizontal: String = "list-group-horizontal"
|
||||
public const val listGroupItem: String = "list-group-item"
|
||||
public const val listGroupItemAction: String = "list-group-item-action"
|
||||
public const val listGroupNumbered: String = "list-group-numbered"
|
||||
public const val navbar: String = "navbar"
|
||||
public const val navbarBrand: String = "navbar-brand"
|
||||
public const val navbarCollapse: String = "navbar-collapse"
|
||||
public const val navLink: String = "nav-link"
|
||||
public const val navbarNav: String = "navbar-nav"
|
||||
public const val navbarToggler: String = "navbar-toggler"
|
||||
public const val navbarTogglerIcon: String = "navbar-toggler-icon"
|
||||
}
|
||||
|
||||
public object Align {
|
||||
public const val baseline: String = "align-baseline"
|
||||
public const val top: String = "align-top"
|
||||
public const val middle: String = "align-middle"
|
||||
public const val bottom: String = "align-bottom"
|
||||
public const val textTop: String = "align-text-top"
|
||||
public const val textBottom: String = "align-text-bottom"
|
||||
}
|
||||
|
||||
public object Display {
|
||||
public val small: DisplayBreakpoint = DisplayBreakpoint(Breakpoint.Small)
|
||||
public val medium: DisplayBreakpoint = DisplayBreakpoint(Breakpoint.Medium)
|
||||
public val extraLarge: DisplayBreakpoint = DisplayBreakpoint(Breakpoint.ExtraLarge)
|
||||
public val extraExtraLarge: DisplayBreakpoint = DisplayBreakpoint(Breakpoint.ExtraExtraLarge)
|
||||
|
||||
public const val none: String = "d-${DisplayValue.none}"
|
||||
public const val inline: String = "d-${DisplayValue.inline}"
|
||||
public const val inlineBlock: String = "d-${DisplayValue.inlineBlock}"
|
||||
public const val block: String = "d-${DisplayValue.block}"
|
||||
public const val grid: String = "d-${DisplayValue.grid}"
|
||||
public const val table: String = "d-${DisplayValue.table}"
|
||||
public const val tableCell: String = "d-${DisplayValue.tableCell}"
|
||||
public const val tableRow: String = "d-${DisplayValue.tableRow}"
|
||||
public const val flex: String = "d-${DisplayValue.flex}"
|
||||
public const val inlineFlex: String = "d-${DisplayValue.inlineFlex}"
|
||||
}
|
||||
|
||||
public class DisplayBreakpoint(breakpoint: Breakpoint) {
|
||||
public val none: String = makeDisplayClass(breakpoint, DisplayValue.none)
|
||||
public val inline: String = makeDisplayClass(breakpoint, DisplayValue.inline)
|
||||
public val inlineBlock: String = makeDisplayClass(breakpoint, DisplayValue.inlineBlock)
|
||||
public val block: String = makeDisplayClass(breakpoint, DisplayValue.block)
|
||||
public val grid: String = makeDisplayClass(breakpoint, DisplayValue.grid)
|
||||
public val table: String = makeDisplayClass(breakpoint, DisplayValue.table)
|
||||
public val tableCell: String = makeDisplayClass(breakpoint, DisplayValue.tableCell)
|
||||
public val tableRow: String = makeDisplayClass(breakpoint, DisplayValue.tableRow)
|
||||
public val flex: String = makeDisplayClass(breakpoint, DisplayValue.flex)
|
||||
public val inlineFlex: String = makeDisplayClass(breakpoint, DisplayValue.inlineFlex)
|
||||
}
|
||||
|
||||
private fun makeDisplayClass(breakpoint: Breakpoint, value: String): String {
|
||||
return "d-$breakpoint-$value"
|
||||
}
|
||||
|
||||
private object DisplayValue {
|
||||
const val none = "none"
|
||||
const val inline = "inline"
|
||||
const val inlineBlock = "inline-block"
|
||||
const val block = "block"
|
||||
const val grid = "grid"
|
||||
const val table = "table"
|
||||
const val tableCell = "table-cell"
|
||||
const val tableRow = "table-row"
|
||||
const val flex = "flex"
|
||||
const val inlineFlex = "inline-flex"
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.attributes.ButtonType
|
||||
import org.jetbrains.compose.web.attributes.disabled
|
||||
import org.jetbrains.compose.web.attributes.type
|
||||
import org.jetbrains.compose.web.dom.Button
|
||||
|
||||
@Composable
|
||||
public fun CloseButton(
|
||||
disabled: Boolean = false,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
Style
|
||||
Button({
|
||||
type(ButtonType.Button)
|
||||
classes("btn-close")
|
||||
attr("aria-label", "Close")
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
onClick {
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.ButtonType
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLButtonElement
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Collapse(
|
||||
title: String,
|
||||
id: String = uuid4().toString(),
|
||||
color: Color = Color.Primary,
|
||||
buttonAttrs: AttrBuilderContext<HTMLButtonElement>? = null,
|
||||
contentAttrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>
|
||||
) {
|
||||
Style
|
||||
needsJS
|
||||
Button(attrs = {
|
||||
classes("btn", "btn-$color")
|
||||
attr("data-bs-toggle", "collapse")
|
||||
attr("data-bs-target", "#_$id")
|
||||
attr("aria-expanded", "false")
|
||||
attr("aria-controls", "_$id")
|
||||
buttonAttrs?.invoke(this)
|
||||
}, type = ButtonType.Button, title = title) { }
|
||||
Div({
|
||||
classes("collapse")
|
||||
id("_$id")
|
||||
contentAttrs?.invoke(this)
|
||||
}, content)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package bootstrap
|
||||
|
||||
public enum class Color(private val value: String) {
|
||||
Primary("primary"),
|
||||
Secondary("secondary"),
|
||||
Success("success"),
|
||||
Info("info"),
|
||||
Warning("warning"),
|
||||
Danger("danger"),
|
||||
Light("light"),
|
||||
Dark("dark"),
|
||||
Body("body"),
|
||||
Muted("muted"),
|
||||
White("white"),
|
||||
Black50("black-50"),
|
||||
White50("white-50"),
|
||||
Transparent("transparent");
|
||||
|
||||
override fun toString(): String = value
|
||||
|
||||
public fun background(): String = "bg-$value"
|
||||
public fun text(): String = "text-$value"
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Column(
|
||||
auto: Boolean = false,
|
||||
breakpoint: Breakpoint? = null,
|
||||
size: Int? = null,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
classes("col" + (breakpoint?.let { "-$it" } ?: "") + (size?.let { "-$it" } ?: ""))
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
if (auto) {
|
||||
classes("col-auto")
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}, content)
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Container(
|
||||
fluid: Boolean = false,
|
||||
type: Breakpoint? = null,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>?
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
when {
|
||||
fluid -> {
|
||||
classes("container-fluid")
|
||||
}
|
||||
type != null -> {
|
||||
classes("container-$type")
|
||||
}
|
||||
else -> {
|
||||
classes("container")
|
||||
}
|
||||
}
|
||||
|
||||
attrs?.invoke(this)
|
||||
}, content)
|
||||
}
|
194
visionforge-compose-html/src/jsMain/kotlin/bootstrap/DropDown.kt
Normal file
194
visionforge-compose-html/src/jsMain/kotlin/bootstrap/DropDown.kt
Normal file
@ -0,0 +1,194 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.AttrsScope
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.jetbrains.compose.web.dom.Button
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.HTMLLIElement
|
||||
import org.w3c.dom.HTMLUListElement
|
||||
|
||||
public class DropDownBuilder(private val scope: ElementScope<HTMLUListElement>) : ElementScope<HTMLUListElement> by scope {
|
||||
@Composable
|
||||
public fun Button(title: String, styling: (Styling.() -> Unit)? = null, onClick: () -> Unit) {
|
||||
Li {
|
||||
val buttonClasses = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
Button({
|
||||
classes("dropdown-item")
|
||||
if (buttonClasses != null) {
|
||||
classes(classes = buttonClasses)
|
||||
}
|
||||
onClick { onClick() }
|
||||
}) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun Divider(styling: (Styling.() -> Unit)? = null) {
|
||||
Li {
|
||||
val buttonClasses = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
Hr {
|
||||
classes("dropdown-divider")
|
||||
if (buttonClasses != null) {
|
||||
classes(classes = buttonClasses)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun Header(title: String, styling: (Styling.() -> Unit)? = null) {
|
||||
Li {
|
||||
val buttonClasses = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
H6({
|
||||
classes("dropdown-header")
|
||||
if (buttonClasses != null) {
|
||||
classes(buttonClasses)
|
||||
}
|
||||
}) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun Custom(block: ContentBuilder<HTMLLIElement>) {
|
||||
Li {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object DropDown {
|
||||
public enum class Direction(private val classname: String) {
|
||||
Down("dropdown"),
|
||||
Up("dropup"),
|
||||
Right("dropend"),
|
||||
Left("dropstart");
|
||||
|
||||
public override fun toString(): String {
|
||||
return classname
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MenuAlignment(internal vararg val classes: String) {
|
||||
public object Start : MenuAlignment("dropdown-menu")
|
||||
public object End : MenuAlignment("dropdown-menu", "dropdown-menu-end")
|
||||
public class StartThenEndAtBreakpoint(breakpoint: Breakpoint) :
|
||||
MenuAlignment("dropdown-menu", "dropdown-menu-$breakpoint-end")
|
||||
|
||||
public class EndThenStartAtBreakpoint(breakpoint: Breakpoint) :
|
||||
MenuAlignment("dropdown-menu", "dropdown-menu-end", "dropdown-menu-$breakpoint-start")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun DropDown(
|
||||
title: String,
|
||||
id: String = remember { "dropdownMenu${uuid4()}" },
|
||||
size: ButtonSize = ButtonSize.Default,
|
||||
color: Color = Color.Primary,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
direction: DropDown.Direction = DropDown.Direction.Down,
|
||||
menuAlignment: DropDown.MenuAlignment = DropDown.MenuAlignment.Start,
|
||||
block: @Composable DropDownBuilder.() -> Unit,
|
||||
) {
|
||||
Style
|
||||
needsJS
|
||||
needsPopper
|
||||
val trigger = @Composable { classes: List<String>? ->
|
||||
Button(attrs = {
|
||||
classes("btn", "btn-$color", "dropdown-toggle")
|
||||
classes(size.toString())
|
||||
if (classes != null) {
|
||||
classes(classes)
|
||||
}
|
||||
id(id)
|
||||
attr("data-bs-toggle", "dropdown")
|
||||
attr("aria-expanded", "false")
|
||||
responsiveAlignmentAttribute(menuAlignment)
|
||||
}) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
|
||||
DropDownBase(trigger, id, styling, direction, menuAlignment, block)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun NavbarDropDown(
|
||||
title: String,
|
||||
href: String?,
|
||||
id: String = remember { "dropdownMenu${uuid4()}" },
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
direction: DropDown.Direction = DropDown.Direction.Down,
|
||||
menuAlignment: DropDown.MenuAlignment = DropDown.MenuAlignment.Start,
|
||||
block: @Composable DropDownBuilder.() -> Unit,
|
||||
) {
|
||||
Style
|
||||
needsJS
|
||||
val trigger = @Composable { classes: List<String>? ->
|
||||
A(
|
||||
attrs = {
|
||||
classes("nav-link", "dropdown-toggle")
|
||||
if (classes != null) {
|
||||
classes(classes)
|
||||
}
|
||||
id(id)
|
||||
attr("data-bs-toggle", "dropdown")
|
||||
attr("aria-expanded", "false")
|
||||
responsiveAlignmentAttribute(menuAlignment)
|
||||
},
|
||||
href = href
|
||||
) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
|
||||
DropDownBase(trigger, id, styling, direction, menuAlignment, block)
|
||||
}
|
||||
|
||||
private fun <T : Element> AttrsScope<T>.responsiveAlignmentAttribute(menuAlignment: DropDown.MenuAlignment) {
|
||||
if (menuAlignment is DropDown.MenuAlignment.EndThenStartAtBreakpoint ||
|
||||
menuAlignment is DropDown.MenuAlignment.StartThenEndAtBreakpoint
|
||||
) {
|
||||
attr("data-bs-display", "static")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DropDownBase(
|
||||
triggerElement: @Composable (classes: List<String>?) -> Unit,
|
||||
id: String,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
direction: DropDown.Direction,
|
||||
menuAlignment: DropDown.MenuAlignment,
|
||||
block: @Composable DropDownBuilder.() -> Unit,
|
||||
) {
|
||||
Style
|
||||
needsJS
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div({ classes("btn-group", direction.toString()) }) {
|
||||
triggerElement(classes)
|
||||
|
||||
Ul({
|
||||
classes(classes = menuAlignment.classes)
|
||||
attr("aria-labelledby", id)
|
||||
}) {
|
||||
DropDownBuilder(this).block()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun FormFloating(
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
Div(attrs = {
|
||||
classes("form-floating")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
if (attrs != null) {
|
||||
attrs()
|
||||
}
|
||||
}, content)
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Label
|
||||
import org.w3c.dom.HTMLLabelElement
|
||||
|
||||
@Composable
|
||||
public fun FormLabel(
|
||||
forId: String? = null,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLLabelElement>? = null,
|
||||
content: ContentBuilder<HTMLLabelElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Label(
|
||||
forId = forId,
|
||||
attrs = {
|
||||
classes(BSClasses.formLabel)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
773
visionforge-compose-html/src/jsMain/kotlin/bootstrap/GridBox.kt
Normal file
773
visionforge-compose-html/src/jsMain/kotlin/bootstrap/GridBox.kt
Normal file
@ -0,0 +1,773 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.css.*
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@Composable
|
||||
public fun GridBox(
|
||||
styling: @Composable GridStyle.() -> Unit,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: (@Composable GridContentBuilder.() -> Unit)? = null
|
||||
) {
|
||||
Style
|
||||
val style = GridStyle(styling)
|
||||
val classes = style.generate()
|
||||
|
||||
Div(attrs = {
|
||||
classes("d-grid")
|
||||
classes(classes = classes)
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
val scope = remember { GridContentBuilder(this) }
|
||||
content?.invoke(scope)
|
||||
}
|
||||
}
|
||||
|
||||
public class GridContentBuilder(scope: ElementScope<HTMLDivElement>) : ElementScope<HTMLDivElement> by scope {
|
||||
public fun Styling.GridItem(spec: GridItemLayout.() -> Unit) {
|
||||
val s = GridItemLayout().apply(spec)
|
||||
|
||||
this.registerGenerator {
|
||||
s.generate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun GridStyle(styling: @Composable GridStyle.() -> Unit): GridStyle = GridStyle().apply { styling() }
|
||||
|
||||
public class GridStyle : Styling() {
|
||||
public val GridLayout: GridLayout = GridLayout()
|
||||
|
||||
@Composable
|
||||
override fun generate(): List<String> {
|
||||
return super.generate() + GridLayout.generate()
|
||||
}
|
||||
}
|
||||
|
||||
public class GridLayout {
|
||||
private var columns: MutableList<GridTemplateTrack> = mutableListOf()
|
||||
private var rows: MutableList<GridTemplateTrack> = mutableListOf()
|
||||
private var areas: MutableList<GridArea> = mutableListOf()
|
||||
public var gap: CSSLengthOrPercentageValue? = null
|
||||
public var justifyContent: Placement? = null
|
||||
public var alignContent: Placement? = null
|
||||
|
||||
public operator fun invoke(f: GridLayout.() -> Unit) {
|
||||
this.f()
|
||||
}
|
||||
|
||||
public enum class Placement(private val value: String) {
|
||||
Start("start"),
|
||||
End("end"),
|
||||
Center("center"),
|
||||
Stretch("stretch"),
|
||||
SpaceAround("space-around"),
|
||||
SpaceBetween("space-between"),
|
||||
SpaceEvenly("space-evenly");
|
||||
|
||||
public override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public fun columns(breakpoint: Breakpoint? = null, spec: GridTemplateTrack.() -> Unit) {
|
||||
columns += GridTemplateTrack(GridTemplateTrack.ColumnOrRow.Column, breakpoint).apply(spec)
|
||||
}
|
||||
|
||||
public fun rows(breakpoint: Breakpoint? = null, spec: GridTemplateTrack.() -> Unit) {
|
||||
rows += GridTemplateTrack(GridTemplateTrack.ColumnOrRow.Row, breakpoint).apply(spec)
|
||||
}
|
||||
|
||||
public fun areas(breakpoint: Breakpoint? = null, spec: GridArea.() -> Unit) {
|
||||
areas += GridArea(breakpoint).apply(spec)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun generate(): List<String> {
|
||||
val classes: MutableList<String> = mutableListOf()
|
||||
|
||||
Style {
|
||||
val classname = remember { "_${uuid4()}" }
|
||||
|
||||
className(classname) style {
|
||||
gap?.let {
|
||||
property("gap", gap.toString())
|
||||
}
|
||||
justifyContent?.let {
|
||||
property("justify-content", it.toString())
|
||||
}
|
||||
alignContent?.let {
|
||||
property("align-content", it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
columns.forEach {
|
||||
it.apply { classes += generateStyle() }
|
||||
}
|
||||
|
||||
rows.forEach {
|
||||
it.apply { classes += generateStyle() }
|
||||
}
|
||||
|
||||
areas.forEach {
|
||||
it.apply { classes += generateStyle() }
|
||||
}
|
||||
classes += classname
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
}
|
||||
|
||||
public class GridTemplateTrack internal constructor(
|
||||
private val type: ColumnOrRow,
|
||||
private val breakpoint: Breakpoint?
|
||||
) {
|
||||
private val items: MutableList<Grid.GridTemplateItem> = mutableListOf()
|
||||
public var gap: CSSLengthOrPercentageValue? = null
|
||||
public var alignment: Alignment? = null
|
||||
private var auto = false
|
||||
|
||||
internal enum class ColumnOrRow(val gap: String, val align: String) {
|
||||
Column("column-gap", "align-items"),
|
||||
Row("row-gap", "justify-items"),
|
||||
}
|
||||
|
||||
public enum class Alignment(private val value: String) {
|
||||
Start("start"),
|
||||
End("end"),
|
||||
Center("center"),
|
||||
Stretch("stretch");
|
||||
|
||||
public override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public fun none() {
|
||||
check(items.isEmpty()) {
|
||||
"Cannot add 'none' to already specified grid-template-columns"
|
||||
}
|
||||
items.add(Grid.GridTemplateNone)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to implement grid-template-columns/rows when the
|
||||
* track list follows the <track-list> syntax.
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
|
||||
*/
|
||||
public fun trackList(block: TrackList.() -> Unit) {
|
||||
check(items.isEmpty()) {
|
||||
"Cannot add more tracks"
|
||||
}
|
||||
items += TrackList().apply(block).items
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to implement grid-template-columns/rows when the
|
||||
* track list follows the <auto-track-list> syntax.
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
|
||||
*/
|
||||
public fun autoTrackList(block: AutoTrackList.() -> Unit) {
|
||||
check(items.isEmpty()) {
|
||||
"Cannot add more tracks"
|
||||
}
|
||||
items += AutoTrackList().apply(block).items
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to implement grid-auto-rows and grid-auto-columns properties.
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
|
||||
*/
|
||||
public fun auto(block: AutoList.() -> Unit) {
|
||||
check(items.isEmpty()) {
|
||||
"Cannot add more tracks"
|
||||
}
|
||||
items += AutoList().apply(block).items
|
||||
auto = true
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StyleSheetBuilder.generateStyle(): String {
|
||||
val classname = remember { "_${uuid4()}" }
|
||||
|
||||
withBreakpoint(breakpoint) {
|
||||
className(classname) style {
|
||||
val propValue = items.joinToString(separator = " ")
|
||||
val propName = when {
|
||||
auto && type == ColumnOrRow.Column -> "grid-auto-columns"
|
||||
auto && type == ColumnOrRow.Row -> "grid-auto-rows"
|
||||
!auto && type == ColumnOrRow.Column -> "grid-template-columns"
|
||||
!auto && type == ColumnOrRow.Row -> "grid-template-rows"
|
||||
else -> error("not possible")
|
||||
}
|
||||
|
||||
property(propName, propValue)
|
||||
|
||||
gap?.let {
|
||||
property(type.gap, it)
|
||||
}
|
||||
|
||||
alignment?.let {
|
||||
property(type.align, it.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classname
|
||||
}
|
||||
}
|
||||
|
||||
public open class TrackListUnits {
|
||||
public val Number.ch: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.ch)
|
||||
public val Number.em: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.em)
|
||||
public val Number.ex: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.ex)
|
||||
public val Number.rem: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.rem)
|
||||
public val Number.vh: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.vh)
|
||||
public val Number.vw: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.vw)
|
||||
public val Number.vmin: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.vmin)
|
||||
public val Number.vmax: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.vmax)
|
||||
public val Number.px: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.px)
|
||||
public val Number.cm: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.cm)
|
||||
public val Number.mm: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.mm)
|
||||
public val Number.pc: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.pc)
|
||||
public val Number.pt: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.pt)
|
||||
public val Number.percent: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.percent)
|
||||
|
||||
public val Number.fr: Grid.Flex
|
||||
get(): Grid.Flex {
|
||||
return Grid.Flex(CSSUnitValueTyped(this.toFloat(), CSSUnit.fr))
|
||||
}
|
||||
|
||||
private class LengthPercentageUnitPropertyDelegate<T : CSSUnitLengthOrPercentage>(val unit: T) {
|
||||
operator fun getValue(thisRef: Number, property: KProperty<*>): Grid.LengthPercentage {
|
||||
return Grid.LengthPercentage(CSSUnitValueTyped(thisRef.toFloat(), unit))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TrackList internal constructor() : TrackListUnits() {
|
||||
internal val items: MutableList<Grid.TrackListItem> = mutableListOf()
|
||||
|
||||
public fun track(names: Array<String>, size: Grid.TrackSizeItem) {
|
||||
if (names.isNotEmpty()) {
|
||||
items += Grid.LineNames(names = names)
|
||||
}
|
||||
items += size
|
||||
}
|
||||
|
||||
public fun track(size: Grid.TrackSizeItem) {
|
||||
items += size
|
||||
}
|
||||
|
||||
public fun track(names: Array<String>, repeat: TrackRepeat) {
|
||||
if (names.isNotEmpty()) {
|
||||
items += Grid.LineNames(names = names)
|
||||
}
|
||||
items += repeat
|
||||
}
|
||||
|
||||
public fun track(repeat: TrackRepeat) {
|
||||
items += repeat
|
||||
}
|
||||
|
||||
public class TrackRepeat(private val count: Int, private vararg val items: Grid.TrackRepeatItem) :
|
||||
Grid.TrackListItem {
|
||||
override fun toString(): String {
|
||||
return "repeat($count, ${items.joinToString(separator = " ")})"
|
||||
}
|
||||
}
|
||||
|
||||
public fun lineNames(vararg names: String) {
|
||||
items += Grid.LineNames(names = names)
|
||||
}
|
||||
}
|
||||
|
||||
public class AutoList internal constructor() : TrackListUnits() {
|
||||
internal val items: MutableList<Grid.TrackSizeItem> = mutableListOf()
|
||||
|
||||
public fun track(size: Grid.TrackSizeItem) {
|
||||
items += size
|
||||
}
|
||||
}
|
||||
|
||||
public data class MinMax(private val min: Grid.InflexibleBreadthItem, private val max: Grid.TrackBreadthItem) :
|
||||
Grid.TrackSizeItem {
|
||||
|
||||
override fun toString(): String {
|
||||
return "minmax($min,$max)"
|
||||
}
|
||||
}
|
||||
|
||||
public class AutoTrackList internal constructor() : TrackListUnits() {
|
||||
internal val items: MutableList<Grid.AutoTrackListItem> = mutableListOf()
|
||||
private var autoRepeat = false
|
||||
|
||||
public fun track(names: List<String>, size: Grid.FixedSizeItem) {
|
||||
if (names.isNotEmpty()) {
|
||||
lineNames(names)
|
||||
}
|
||||
items += size
|
||||
}
|
||||
|
||||
public fun track(size: Grid.FixedSizeItem) {
|
||||
items += size
|
||||
}
|
||||
|
||||
public fun track(names: List<String>, repeat: FixedRepeat) {
|
||||
if (names.isNotEmpty()) {
|
||||
lineNames(names)
|
||||
}
|
||||
items += repeat
|
||||
}
|
||||
|
||||
public fun track(repeat: FixedRepeat) {
|
||||
items += repeat
|
||||
}
|
||||
|
||||
public fun lineNames(names: List<String>) {
|
||||
items += Grid.LineNames(names = names.toTypedArray())
|
||||
}
|
||||
|
||||
public fun lineNames(vararg names: String) {
|
||||
items += Grid.LineNames(*names)
|
||||
}
|
||||
|
||||
public fun autoRepeat(type: RepeatType, vararg repeatItems: Grid.FixedRepeatItem) {
|
||||
if (type == RepeatType.AutoFill || type == RepeatType.AutoFit) {
|
||||
require(!autoRepeat) {
|
||||
"auto-repeat already specified, can only be specified once per grid-template-columns"
|
||||
}
|
||||
autoRepeat = true
|
||||
}
|
||||
|
||||
items += AutoRepeat(type, *repeatItems)
|
||||
}
|
||||
|
||||
public class MinMax private constructor(private val min: Any, private val max: Any) : Grid.FixedSizeItem {
|
||||
public constructor(minimum: Grid.FixedBreadthItem, maximum: Grid.TrackBreadthItem) : this(
|
||||
min = minimum,
|
||||
max = maximum
|
||||
)
|
||||
|
||||
public constructor(minimum: Grid.InflexibleBreadthItem, maximum: Grid.FixedBreadthItem) : this(
|
||||
min = minimum,
|
||||
max = maximum
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
return "minmax($min,$max)"
|
||||
}
|
||||
}
|
||||
|
||||
public class AutoRepeat(private val type: RepeatType, private vararg val items: Grid.FixedRepeatItem) :
|
||||
Grid.AutoTrackListItem {
|
||||
override fun toString(): String {
|
||||
return "repeat($type, ${items.joinToString(separator = " ")})"
|
||||
}
|
||||
}
|
||||
|
||||
public enum class RepeatType(private val value: String) {
|
||||
AutoFill("auto-fill"),
|
||||
AutoFit("auto-fit");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public class FixedRepeat(private val count: Int, private vararg val items: Grid.FixedRepeatItem) :
|
||||
Grid.AutoTrackListItem {
|
||||
override fun toString(): String {
|
||||
return "repeat($count, ${items.joinToString(separator = " ")})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public object Grid {
|
||||
/**
|
||||
* Marker interface for items that can be included in a Grid-Template-Columns or
|
||||
* Grid-Template-Rows propery.
|
||||
*/
|
||||
public interface GridTemplateItem
|
||||
|
||||
/**
|
||||
* Marker interface for items that can be included in a <track-list>.
|
||||
* see https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
|
||||
*/
|
||||
public interface TrackListItem : GridTemplateItem
|
||||
|
||||
/**
|
||||
* Marker interface for items that can be included in an <auto-track-list>.
|
||||
* see https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
|
||||
*/
|
||||
public interface AutoTrackListItem : GridTemplateItem
|
||||
|
||||
// Marker interfaces for items that can be used as a <track-size>
|
||||
public interface TrackSizeItem : TrackListItem, TrackRepeatItem
|
||||
public interface TrackBreadthItem : TrackSizeItem
|
||||
public interface TrackRepeatItem
|
||||
|
||||
// Marker interfaces for items that can be used as a <fixed-size>
|
||||
public interface FixedSizeItem : AutoTrackListItem, FixedRepeatItem
|
||||
public interface FixedBreadthItem : FixedSizeItem
|
||||
public interface FixedRepeatItem
|
||||
|
||||
public interface InflexibleBreadthItem
|
||||
|
||||
internal object GridTemplateNone : GridTemplateItem {
|
||||
override fun toString(): String {
|
||||
return "none"
|
||||
}
|
||||
}
|
||||
|
||||
public class LineNames(private vararg val names: String) :
|
||||
TrackListItem, AutoTrackListItem, TrackRepeatItem, FixedRepeatItem {
|
||||
override fun toString(): String {
|
||||
return names.joinToString(prefix = "[", postfix = "]", separator = " ")
|
||||
}
|
||||
}
|
||||
|
||||
public data class FitContent(val value: LengthPercentage) : TrackSizeItem {
|
||||
override fun toString(): String {
|
||||
return "fit-content($value)"
|
||||
}
|
||||
}
|
||||
|
||||
public class LengthPercentage(private val v: CSSLengthOrPercentageValue) :
|
||||
InflexibleBreadthItem, TrackBreadthItem, FixedBreadthItem {
|
||||
override fun toString(): String {
|
||||
return v.toString()
|
||||
}
|
||||
}
|
||||
|
||||
public object MinContent : InflexibleBreadthItem, TrackBreadthItem {
|
||||
override fun toString(): String {
|
||||
return "min-content"
|
||||
}
|
||||
}
|
||||
|
||||
public object MaxContent : InflexibleBreadthItem, TrackBreadthItem {
|
||||
override fun toString(): String {
|
||||
return "max-content"
|
||||
}
|
||||
}
|
||||
|
||||
public object Auto : InflexibleBreadthItem, TrackBreadthItem {
|
||||
override fun toString(): String {
|
||||
return "auto"
|
||||
}
|
||||
}
|
||||
|
||||
public class Flex(private val v: CSSSizeValue<CSSUnit.fr>) : TrackBreadthItem {
|
||||
override fun toString(): String {
|
||||
return v.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class GridArea internal constructor(private val breakpoint: Breakpoint?) {
|
||||
private val rows: MutableList<List<String>> = mutableListOf()
|
||||
|
||||
public fun row(vararg cells: String) {
|
||||
rows += listOf(*cells)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StyleSheetBuilder.generateStyle(): String {
|
||||
val classname = remember { "_${uuid4()}" }
|
||||
|
||||
withBreakpoint(breakpoint) {
|
||||
className(classname) style {
|
||||
property(
|
||||
"grid-template-areas",
|
||||
rows.joinToString(separator = " ") {
|
||||
it.joinToString(separator = " ", prefix = "\"", postfix = "\"")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return classname
|
||||
}
|
||||
}
|
||||
|
||||
public class GridItemLayout {
|
||||
private val areaSpecs: MutableList<GridItemArea> = mutableListOf()
|
||||
private val placements: MutableList<PlacementSpec> = mutableListOf()
|
||||
|
||||
public fun area(breakpoint: Breakpoint? = null, name: String) {
|
||||
areaSpecs += GridItemArea(breakpoint, name)
|
||||
}
|
||||
|
||||
public fun area(name: String) {
|
||||
areaSpecs += GridItemArea(null, name)
|
||||
}
|
||||
|
||||
public fun area(breakpoint: Breakpoint, name: String) {
|
||||
areaSpecs += GridItemArea(breakpoint, name)
|
||||
}
|
||||
|
||||
public fun area(spec: GridItemArea.() -> Unit) {
|
||||
areaSpecs += GridItemArea().apply(spec)
|
||||
}
|
||||
|
||||
public fun area(breakpoint: Breakpoint, spec: GridItemArea.() -> Unit) {
|
||||
areaSpecs += GridItemArea(breakpoint).apply(spec)
|
||||
}
|
||||
|
||||
public fun placement(spec: PlacementSpec.() -> Unit) {
|
||||
placements += PlacementSpec().apply(spec)
|
||||
}
|
||||
|
||||
public fun placement(breakpoint: Breakpoint, spec: PlacementSpec.() -> Unit) {
|
||||
placements += PlacementSpec(breakpoint).apply(spec)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun generate(): List<String> {
|
||||
val classes: MutableList<String> = mutableListOf()
|
||||
|
||||
Style {
|
||||
areaSpecs.forEach {
|
||||
it.apply { classes += generateStyle() }
|
||||
}
|
||||
placements.forEach {
|
||||
it.apply { classes += generateStyle() }
|
||||
}
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
}
|
||||
|
||||
public class GridItemArea(private val breakpoint: Breakpoint? = null, private val name: String? = null) {
|
||||
private var column: GridItemAreaSpec? = null
|
||||
private var row: GridItemAreaSpec? = null
|
||||
|
||||
public fun row(f: GridItemAreaSpec.() -> Unit) {
|
||||
row = GridItemAreaSpec().apply(f)
|
||||
}
|
||||
|
||||
public fun row(start: Int, end: Int? = null) {
|
||||
row = GridItemAreaSpec().apply {
|
||||
start(start)
|
||||
end?.let { end(it) }
|
||||
}
|
||||
}
|
||||
|
||||
public fun row(start: Int, end: GridLine? = null) {
|
||||
row = GridItemAreaSpec().apply {
|
||||
start(start)
|
||||
end(end)
|
||||
}
|
||||
}
|
||||
|
||||
public fun row(start: GridLine, end: Int? = null) {
|
||||
row = GridItemAreaSpec().apply {
|
||||
start(start)
|
||||
end?.let {
|
||||
end(end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun row(start: GridLine, end: GridLine?) {
|
||||
row = GridItemAreaSpec().apply {
|
||||
start(start)
|
||||
end(end)
|
||||
}
|
||||
}
|
||||
|
||||
public fun column(f: GridItemAreaSpec.() -> Unit) {
|
||||
column = GridItemAreaSpec().apply(f)
|
||||
}
|
||||
|
||||
public fun column(start: Int, end: Int?) {
|
||||
column = GridItemAreaSpec().apply {
|
||||
start(start)
|
||||
end?.let {
|
||||
end(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun column(start: Int, end: GridLine?) {
|
||||
column = GridItemAreaSpec().apply {
|
||||
start(start)
|
||||
end(end)
|
||||
}
|
||||
}
|
||||
|
||||
public fun column(start: GridLine, end: Int?) {
|
||||
column = GridItemAreaSpec().apply {
|
||||
start(start)
|
||||
end?.let { end(it) }
|
||||
}
|
||||
}
|
||||
|
||||
public fun column(start: GridLine, end: GridLine) {
|
||||
column = GridItemAreaSpec().apply {
|
||||
start(start)
|
||||
end(end)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StyleSheetBuilder.generateStyle(): String {
|
||||
val classname = remember { "_${uuid4()}" }
|
||||
|
||||
withBreakpoint(breakpoint) {
|
||||
className(classname) style {
|
||||
if (name != null) {
|
||||
property("grid-area", name)
|
||||
} else {
|
||||
column?.let {
|
||||
property("grid-column", it.toString())
|
||||
}
|
||||
row?.let {
|
||||
property("grid-row", it.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classname
|
||||
}
|
||||
}
|
||||
|
||||
public class GridItemAreaSpec {
|
||||
private var startPlacement: GridLine = GridLine.Auto
|
||||
private var endPlacement: GridLine? = null
|
||||
|
||||
public fun start(ident: String) {
|
||||
startPlacement = GridLine.CustomIdent(ident)
|
||||
}
|
||||
|
||||
public fun start(line: Int, ident: String? = null) {
|
||||
startPlacement = GridLine.Line(line, ident)
|
||||
}
|
||||
|
||||
public fun end(ident: String) {
|
||||
endPlacement = GridLine.CustomIdent(ident)
|
||||
}
|
||||
|
||||
public fun end(line: Int, ident: String? = null) {
|
||||
endPlacement = GridLine.Line(line, ident)
|
||||
}
|
||||
|
||||
internal fun start(start: GridLine) {
|
||||
startPlacement = start
|
||||
}
|
||||
|
||||
internal fun end(end: GridLine?) {
|
||||
end?.let {
|
||||
endPlacement = it
|
||||
}
|
||||
}
|
||||
|
||||
public infix fun Int.to(end: Int) {
|
||||
startPlacement = GridLine.Line(this)
|
||||
endPlacement = GridLine.Line(end)
|
||||
}
|
||||
|
||||
public infix fun Int.to(end: GridLine) {
|
||||
startPlacement = GridLine.Line(this)
|
||||
endPlacement = end
|
||||
}
|
||||
|
||||
public infix fun GridLine.to(end: GridLine) {
|
||||
startPlacement = this
|
||||
endPlacement = end
|
||||
}
|
||||
|
||||
public infix fun GridLine.to(end: Int) {
|
||||
startPlacement = this
|
||||
endPlacement = GridLine.Line(end)
|
||||
}
|
||||
|
||||
public override fun toString(): String {
|
||||
return startPlacement.toString() + (endPlacement?.let { " / $endPlacement" } ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GridLine {
|
||||
public object Auto : GridLine() {
|
||||
override fun toString(): String {
|
||||
return "auto"
|
||||
}
|
||||
}
|
||||
|
||||
internal data class CustomIdent(private val ident: String) : GridLine() {
|
||||
override fun toString(): String {
|
||||
return ident
|
||||
}
|
||||
}
|
||||
|
||||
internal data class Line(private val number: Int, private val ident: String? = null) : GridLine() {
|
||||
override fun toString(): String {
|
||||
return number.toString() + (ident ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
public class Span private constructor(private val number: Int?, private val ident: String?) :
|
||||
GridLine() {
|
||||
|
||||
public constructor(n: Int) : this(n, null)
|
||||
public constructor(id: String) : this(null, id)
|
||||
|
||||
override fun toString(): String {
|
||||
return "span " + (number?.toString() ?: ident)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PlacementSpec(private val breakpoint: Breakpoint? = null) {
|
||||
public var block: PlacementType = PlacementType.Auto
|
||||
public var inline: PlacementType? = null
|
||||
|
||||
public enum class PlacementType(private val value: String) {
|
||||
Auto("auto"),
|
||||
Start("start"),
|
||||
End("end"),
|
||||
Center("center"),
|
||||
Stretch("stretch");
|
||||
|
||||
public override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StyleSheetBuilder.generateStyle(): String {
|
||||
val classname = remember { "_${uuid4()}" }
|
||||
|
||||
withBreakpoint(breakpoint) {
|
||||
className(classname) style {
|
||||
property("place-self", block.toString() + (inline?.let { " $it" } ?: ""))
|
||||
}
|
||||
}
|
||||
|
||||
return classname
|
||||
}
|
||||
}
|
||||
|
||||
private fun StyleSheetBuilder.withBreakpoint(
|
||||
breakpoint: Breakpoint?,
|
||||
block: GenericStyleSheetBuilder<CSSStyleRuleBuilder>.() -> Unit
|
||||
) {
|
||||
val bp = breakpoints[breakpoint]
|
||||
if (bp != null) {
|
||||
media(mediaMinWidth(bp)) {
|
||||
block()
|
||||
}
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
30
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Icon.kt
Normal file
30
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Icon.kt
Normal file
@ -0,0 +1,30 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.I
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
/**
|
||||
* Bootstrap Icon shortcut when using with already embedded icons.
|
||||
* Alternative use `bootstrap-compose-icons`.
|
||||
*/
|
||||
@Composable
|
||||
public fun Icon(
|
||||
iconName: String,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: AttrBuilderContext<HTMLElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
I({
|
||||
classes("bi", "bi-$iconName")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrsBuilder?.invoke(this)
|
||||
})
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.attributes.AutoComplete
|
||||
import org.jetbrains.compose.web.attributes.InputType
|
||||
import org.jetbrains.compose.web.attributes.autoComplete
|
||||
import org.jetbrains.compose.web.attributes.builders.InputAttrsScope
|
||||
import org.jetbrains.compose.web.attributes.placeholder
|
||||
import org.jetbrains.compose.web.dom.Input
|
||||
import org.jetbrains.compose.web.dom.Label
|
||||
import org.jetbrains.compose.web.events.SyntheticInputEvent
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.Text
|
||||
|
||||
@Composable
|
||||
public fun <T> Input(
|
||||
label: String,
|
||||
value: String,
|
||||
type: InputType<T>,
|
||||
placeholder: String? = null,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
labelClasses: String = "form-label",
|
||||
inputClasses: String = "form-control",
|
||||
attrs: (InputAttrsScope<T>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<T, HTMLInputElement>) -> Unit
|
||||
) {
|
||||
Style
|
||||
Label(forId = null, attrs = {
|
||||
classes(labelClasses)
|
||||
}) {
|
||||
Text(label)
|
||||
Input(type = type, attrs = {
|
||||
autoComplete(autocomplete)
|
||||
attrs?.invoke(this)
|
||||
classes(inputClasses)
|
||||
value(value)
|
||||
if (placeholder != null) {
|
||||
placeholder(placeholder)
|
||||
}
|
||||
onInput {
|
||||
onInput(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,561 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.*
|
||||
import org.jetbrains.compose.web.attributes.builders.InputAttrsScope
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
import org.jetbrains.compose.web.events.SyntheticInputEvent
|
||||
import org.w3c.dom.*
|
||||
|
||||
@Composable
|
||||
public fun InputGroup(
|
||||
inputId: String = remember { "_${uuid4()}" },
|
||||
size: InputGroupSize = InputGroupSize.Default,
|
||||
content: @Composable InputGroupContext.() -> Unit,
|
||||
) {
|
||||
Style
|
||||
val scope = InputGroupContext(inputId)
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
classes(BSClasses.inputGroup)
|
||||
when (size) {
|
||||
InputGroupSize.Small -> classes(BSClasses.inputGroupSmall)
|
||||
InputGroupSize.Large -> classes(BSClasses.inputGroupLarge)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
scope.content()
|
||||
}
|
||||
}
|
||||
|
||||
public class InputGroupContext(private val inputId: String) {
|
||||
private fun <K> InputAttrsScope<K>.buildInputAttrs(
|
||||
disabled: Boolean = false,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
classes: List<String>?,
|
||||
attrs: (InputAttrsScope<K>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<K, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
classes(BSClasses.formControl)
|
||||
if (classes != null) {
|
||||
classes(classes)
|
||||
}
|
||||
id(inputId)
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
autoComplete(autocomplete)
|
||||
attrs?.invoke(this)
|
||||
onInput { event ->
|
||||
onInput(event)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun <K> Input(
|
||||
type: InputType<K>,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: (InputAttrsScope<K>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<K, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Input(
|
||||
type = type,
|
||||
attrs = {
|
||||
buildInputAttrs(false, autocomplete, classes, attrs, onInput)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun DateInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.DateInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun DateTimeLocalInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.DateTimeLocalInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun EmailInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.EmailInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun FileInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.FileInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun HiddenInput(
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.HiddenInput {
|
||||
buildInputAttrs(disabled, autocomplete = AutoComplete.off, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun MonthInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.MonthInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun NumberInput(
|
||||
value: Number?,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
min: Number? = null,
|
||||
max: Number? = null,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<Number?>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<Number?, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
NumberInput(value, min, max) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun PasswordInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.PasswordInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun SearchInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.SearchInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun TelInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.TelInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun TextInput(
|
||||
value: String,
|
||||
placeholder: String? = null,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.TextInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrs, onInput)
|
||||
placeholder?.let {
|
||||
placeholder(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun TextAreaInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLTextAreaElement>? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
TextArea(attrs = {
|
||||
classes(BSClasses.formControl)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
autoComplete(autocomplete)
|
||||
id(inputId)
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
onInput { event ->
|
||||
onInput(event)
|
||||
}
|
||||
}, value = value)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun TimeInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.TimeInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrs, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun UrlInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.UrlInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrs, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun WeekInput(
|
||||
value: String,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: (InputAttrsScope<String>.() -> Unit)? = null,
|
||||
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.WeekInput(value) {
|
||||
buildInputAttrs(disabled, autocomplete, classes, attrs, onInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun SelectInput(
|
||||
disabled: Boolean,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLSelectElement>? = null,
|
||||
onChange: (List<String>) -> Unit,
|
||||
content: @Composable SelectContext.() -> Unit,
|
||||
) {
|
||||
Style
|
||||
Select(
|
||||
disabled = disabled,
|
||||
id = inputId,
|
||||
autocomplete = autocomplete,
|
||||
styling = styling,
|
||||
attrs = attrs,
|
||||
onChange = onChange,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Input group add on as text in a Span element.
|
||||
*/
|
||||
@Composable
|
||||
public fun TextAddOn(
|
||||
text: String,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLSpanElement>? = null,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Span(attrs = {
|
||||
classes(BSClasses.inputGroupText)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Input group add on as text in a Label element. The label element's forId is set equal to the
|
||||
* Input's id. An example of this usage is in the Custom select Bootstrap documentation
|
||||
* at https://getbootstrap.com/docs/5.0/forms/input-group/#custom-select.
|
||||
*/
|
||||
@Composable
|
||||
public fun LabelAddOn(
|
||||
text: String,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLLabelElement>? = null,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Label(attrs = {
|
||||
classes(BSClasses.inputGroupText)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
forId(inputId)
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun ButtonAddOn(
|
||||
title: String,
|
||||
color: Color = Color.Primary,
|
||||
type: ButtonType = ButtonType.Submit,
|
||||
size: ButtonSize = ButtonSize.Default,
|
||||
disabled: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
|
||||
action: () -> Unit,
|
||||
) {
|
||||
Style
|
||||
Button(title, color, size, false, type, disabled, styling, attrs, action)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun DropDownAddOn(
|
||||
title: String,
|
||||
color: Color = Color.Primary,
|
||||
size: ButtonSize = ButtonSize.Default,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
block: @Composable DropDownBuilder.() -> Unit,
|
||||
) {
|
||||
Style
|
||||
DropDown(title, inputId, size, color, styling, block = block)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun CheckboxAddOn(
|
||||
checked: Boolean,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
onClick: (Boolean) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
classes(BSClasses.inputGroupText)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
CheckboxInput(checked, attrs = {
|
||||
onInput { event ->
|
||||
onClick(event.value)
|
||||
}
|
||||
classes(BSClasses.formCheckInput, "mt-0")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun RadioAddOn(
|
||||
checked: Boolean,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
onClick: (Boolean) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
classes(BSClasses.inputGroupText)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
RadioInput(checked, attrs = {
|
||||
onInput { event ->
|
||||
onClick(event.value)
|
||||
}
|
||||
classes(BSClasses.formCheckInput, "mt-0")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum class InputGroupSize {
|
||||
Small,
|
||||
Default,
|
||||
Large
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.attributes.AttrsScope
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.*
|
||||
|
||||
@Composable
|
||||
public fun ListGroup(
|
||||
flush: Boolean = false,
|
||||
listGroupDirection: ListGroupDirection = ListGroupDirection.Vertical,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLUListElement>? = null,
|
||||
content: ContentBuilder<HTMLUListElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Ul(
|
||||
attrs = {
|
||||
ListGroupAttrs(flush, false, listGroupDirection, attrs)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun NumberedListGroup(
|
||||
flush: Boolean = false,
|
||||
listGroupDirection: ListGroupDirection = ListGroupDirection.Vertical,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLOListElement>? = null,
|
||||
content: ContentBuilder<HTMLOListElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Ol(
|
||||
attrs = {
|
||||
ListGroupAttrs(flush, true, listGroupDirection, attrs)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T : HTMLElement> AttrsScope<T>.ListGroupAttrs(
|
||||
flush: Boolean = false,
|
||||
numbered: Boolean = false,
|
||||
listGroupDirection: ListGroupDirection = ListGroupDirection.Vertical,
|
||||
attrs: AttrBuilderContext<T>? = null,
|
||||
) {
|
||||
classes(BSClasses.listGroup)
|
||||
if (flush) {
|
||||
classes(BSClasses.listGroupFlush)
|
||||
}
|
||||
if (numbered) {
|
||||
classes(BSClasses.listGroupNumbered)
|
||||
}
|
||||
|
||||
if (listGroupDirection is ListGroupDirection.Horizontal) {
|
||||
classes(listGroupDirection.classname())
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun ListItem(
|
||||
active: Boolean = false,
|
||||
disabled: Boolean = false,
|
||||
background: Color? = null,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLLIElement>? = null,
|
||||
content: ContentBuilder<HTMLLIElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Li(attrs = {
|
||||
ListItemAttrs(active, disabled, false, background, attrs)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
}, content = content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun AnchorListItem(
|
||||
href: String? = null,
|
||||
active: Boolean = false,
|
||||
disabled: Boolean = false,
|
||||
background: Color? = null,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLAnchorElement>? = null,
|
||||
content: ContentBuilder<HTMLAnchorElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
A(
|
||||
href = href,
|
||||
attrs = {
|
||||
ListItemAttrs(active, disabled, true, background, attrs)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun ButtonListItem(
|
||||
active: Boolean = false,
|
||||
disabled: Boolean = false,
|
||||
background: Color? = null,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
|
||||
content: ContentBuilder<HTMLButtonElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Button(attrs = {
|
||||
ListItemAttrs(active, disabled, true, background, attrs)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
}, content = content)
|
||||
}
|
||||
|
||||
private fun <T : HTMLElement> AttrsScope<T>.ListItemAttrs(
|
||||
active: Boolean = false,
|
||||
disabled: Boolean = false,
|
||||
actionable: Boolean = false,
|
||||
background: Color? = null,
|
||||
attrs: AttrBuilderContext<T>? = null,
|
||||
) {
|
||||
classes(BSClasses.listGroupItem)
|
||||
if (active) {
|
||||
classes(BSClasses.active)
|
||||
attr("aria-current", "true")
|
||||
}
|
||||
if (disabled) {
|
||||
classes(BSClasses.disabled)
|
||||
attr("aria-disabled", "true")
|
||||
}
|
||||
if (actionable) {
|
||||
classes(BSClasses.listGroupItemAction)
|
||||
}
|
||||
background?.let {
|
||||
classes("${BSClasses.listGroupItem}-$it")
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a ListGroup's direction, and optionally for the Horizontal variant a breakpoint.
|
||||
*/
|
||||
public sealed class ListGroupDirection {
|
||||
public object Vertical : ListGroupDirection()
|
||||
public data class Horizontal(val breakpoint: Breakpoint? = null) : ListGroupDirection() {
|
||||
internal fun classname(): String {
|
||||
return "${BSClasses.listGroupHorizontal}${breakpoint?.let { "-$it" } ?: ""}"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Modal(
|
||||
header: String,
|
||||
size: Breakpoint? = null,
|
||||
scrollable: Boolean = false,
|
||||
id: String = uuid4().toString(),
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
onDismissRequest: () -> Unit,
|
||||
footer: ContentBuilder<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>,
|
||||
) {
|
||||
Style
|
||||
needsJS
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div({
|
||||
classes("modal")
|
||||
id("_$id")
|
||||
tabIndex(-1)
|
||||
attr("aria-labelledby", "label$id")
|
||||
attr("aria-hidden", "true")
|
||||
attr("data-bs-backdrop", "static")
|
||||
attr("data-bs-keyboard", "false")
|
||||
}) {
|
||||
DisposableEffect(true) {
|
||||
val htmlDivElement: HTMLDivElement = scopeElement
|
||||
val bsModal = Modal(htmlDivElement)
|
||||
htmlDivElement.addEventListener("hidePrevented.bs.modal", callback = { _ ->
|
||||
onDismissRequest()
|
||||
})
|
||||
bsModal.show()
|
||||
onDispose {
|
||||
bsModal.hide()
|
||||
bsModal.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Div({
|
||||
classes("modal-dialog")
|
||||
if (size != null) {
|
||||
classes("modal-$size")
|
||||
}
|
||||
if (scrollable) {
|
||||
classes("modal-dialog-scrollable")
|
||||
}
|
||||
}) {
|
||||
Div({
|
||||
classes("modal-content")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
}) {
|
||||
Div({ classes("modal-header") }) {
|
||||
H5({
|
||||
classes("modal-title")
|
||||
id("label$id")
|
||||
}) {
|
||||
Text(header)
|
||||
}
|
||||
Button({
|
||||
classes("btn-close")
|
||||
// attr("data-bs-dismiss", "modal")
|
||||
attr("aria-label", "Close")
|
||||
onClick {
|
||||
onDismissRequest()
|
||||
}
|
||||
})
|
||||
}
|
||||
Div({ classes("modal-body") }, content)
|
||||
Div({ classes("modal-footer") }, footer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
221
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Navbar.kt
Normal file
221
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Navbar.kt
Normal file
@ -0,0 +1,221 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.*
|
||||
|
||||
/**
|
||||
* Bootstrap Navbar component. This version provides more flexibility than the alternative but is more complex
|
||||
* to use. For a simpler implementation use the version that takes a brand and navItems as parameters.
|
||||
*
|
||||
* @param collapseBehavior Specifies the Navbar's Responsive behavior with use of the .navbar-expand class.
|
||||
* @param fluid Specifies if the inner container is fluid (container-fluid) or not.
|
||||
* @param placement Specifies the placement of the navbar
|
||||
* @param containerBreakpoint Breakpoint for the inner container.
|
||||
* @param colorScheme Valid values are Color.Light or Color.Dark to set the navbar-dark/light class.
|
||||
* @param backgroundColor Background color to use, with the bg-* classes.
|
||||
* @param content Navbar content, placed within the inner container.
|
||||
*/
|
||||
@Composable
|
||||
public fun Navbar(
|
||||
collapseBehavior: NavbarCollapseBehavior = NavbarCollapseBehavior.Always,
|
||||
fluid: Boolean = false,
|
||||
placement: NavbarPlacement = NavbarPlacement.Default,
|
||||
containerBreakpoint: Breakpoint? = null,
|
||||
colorScheme: Color = Color.Light,
|
||||
backgroundColor: Color = Color.Primary,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>? = null,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Nav(attrs = {
|
||||
classes(
|
||||
BSClasses.navbar,
|
||||
"navbar-$colorScheme",
|
||||
"bg-$backgroundColor"
|
||||
)
|
||||
when (placement) {
|
||||
NavbarPlacement.Default -> {
|
||||
}
|
||||
|
||||
else -> {
|
||||
classes(placement.toString())
|
||||
}
|
||||
}
|
||||
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
when (collapseBehavior) {
|
||||
is NavbarCollapseBehavior.Never -> classes("navbar-expand")
|
||||
is NavbarCollapseBehavior.AtBreakpoint -> classes("navbar-expand-${collapseBehavior.breakpoint}")
|
||||
is NavbarCollapseBehavior.Always -> {
|
||||
} // No class needed for "Always" behavior
|
||||
}
|
||||
attr("role", "navigation")
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
Container(fluid, containerBreakpoint, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap Navbar component that can optionally enable a Toggler and Brand.
|
||||
*
|
||||
* @param collapseBehavior Specifies the Navbar's Responsive behavior with use of the .navbar-expand class.
|
||||
* @param fluid Specifies if the inner container is fluid (container-fluid) or not.
|
||||
* @param stickyTop if true, shows the navbar at top
|
||||
* @param containerBreakpoint Breakpoint for the inner container.
|
||||
* @param colorScheme Valid values are Color.Light or Color.Dark to set the navbar-dark/light class.
|
||||
* @param backgroundColor Background color to use, with the bg-* classes.
|
||||
* @param toggler Whether a toggler button should be provided when falling below the breakpoint.
|
||||
* @param togglerPosition Specifies if the toggler should be placed on the left or right side of the Navbar.
|
||||
* @param togglerAttrs Additional attributes to set on the toggler button
|
||||
* @param togglerTargetId Optional id of the toggler, using a random UUID by default
|
||||
* @param attrs Additional attributes to set on the Navbar
|
||||
* @param navAttrs Additional attributes to set on the navItems section of the Navbar
|
||||
* @param brand Composable implementing the brand elements
|
||||
* @param navItems navigation items, normally comprised of NavbarLink and NavbarDropDown menu items.
|
||||
*/
|
||||
@Composable
|
||||
public fun Navbar(
|
||||
collapseBehavior: NavbarCollapseBehavior = NavbarCollapseBehavior.Always,
|
||||
fluid: Boolean = false,
|
||||
placement: NavbarPlacement = NavbarPlacement.Default,
|
||||
containerBreakpoint: Breakpoint? = null,
|
||||
colorScheme: Color = Color.Light,
|
||||
backgroundColor: Color = Color.Primary,
|
||||
toggler: Boolean = true,
|
||||
togglerPosition: TogglerPosition = TogglerPosition.Right,
|
||||
togglerTargetId: String = remember { "toggler${uuid4()}" },
|
||||
togglerAttrs: AttrBuilderContext<HTMLButtonElement>? = null,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLElement>? = null,
|
||||
navAttrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
additionalNavContent: ContentBuilder<HTMLDivElement>? = null,
|
||||
brand: ContentBuilder<HTMLDivElement>,
|
||||
navItems: ContentBuilder<HTMLDivElement>,
|
||||
) {
|
||||
Navbar(
|
||||
collapseBehavior,
|
||||
fluid,
|
||||
placement,
|
||||
containerBreakpoint,
|
||||
colorScheme,
|
||||
backgroundColor,
|
||||
styling,
|
||||
attrs
|
||||
) {
|
||||
if (togglerPosition == TogglerPosition.Right) {
|
||||
brand()
|
||||
}
|
||||
|
||||
if (toggler) {
|
||||
Toggler(
|
||||
target = togglerTargetId,
|
||||
controls = togglerTargetId,
|
||||
styling = styling,
|
||||
attrs = togglerAttrs
|
||||
)
|
||||
NavbarCollapse(togglerTargetId) {
|
||||
NavbarNav(attrs = { navAttrs?.invoke(this) }) {
|
||||
navItems()
|
||||
}
|
||||
if (additionalNavContent != null) {
|
||||
additionalNavContent()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavbarNav(attrs = { navAttrs?.invoke(this) }) {
|
||||
navItems()
|
||||
}
|
||||
if (additionalNavContent != null) {
|
||||
additionalNavContent()
|
||||
}
|
||||
}
|
||||
|
||||
if (togglerPosition == TogglerPosition.Left) {
|
||||
brand()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap navbar-collapse component to be used with the NavbarToggler.
|
||||
* @param id The element id. This value should also be used as the target parameter with a NavbarToggler.
|
||||
*/
|
||||
@Composable
|
||||
public fun NavbarCollapse(
|
||||
id: String,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>? = null,
|
||||
) {
|
||||
Style
|
||||
Div(attrs = {
|
||||
classes(BSClasses.collapse, BSClasses.navbarCollapse)
|
||||
id(id)
|
||||
attrs?.invoke(this)
|
||||
}, content = content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap navbar-nav component to be used within a Navbar.
|
||||
*/
|
||||
@Composable
|
||||
public fun NavbarNav(
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
links: ContentBuilder<HTMLDivElement>? = null,
|
||||
) {
|
||||
Style
|
||||
Div(attrs = {
|
||||
classes(BSClasses.navbarNav)
|
||||
attrs?.invoke(this)
|
||||
}, content = links)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap nav-link component.
|
||||
*/
|
||||
@Composable
|
||||
public fun NavbarLink(
|
||||
active: Boolean,
|
||||
attrs: AttrBuilderContext<HTMLAnchorElement>? = null,
|
||||
disabled: Boolean = false,
|
||||
link: String? = null,
|
||||
content: ContentBuilder<HTMLAnchorElement>? = null,
|
||||
) {
|
||||
Style
|
||||
A(attrs = {
|
||||
classes(BSClasses.navLink)
|
||||
if (active) {
|
||||
classes(BSClasses.active)
|
||||
attr("aria-current", "page")
|
||||
}
|
||||
if (disabled) {
|
||||
classes(BSClasses.disabled)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}, href = link, content = content)
|
||||
}
|
||||
|
||||
public enum class TogglerPosition {
|
||||
Left, Right
|
||||
}
|
||||
|
||||
public sealed class NavbarCollapseBehavior {
|
||||
public object Never : NavbarCollapseBehavior()
|
||||
public object Always : NavbarCollapseBehavior()
|
||||
public data class AtBreakpoint(val breakpoint: Breakpoint) : NavbarCollapseBehavior()
|
||||
}
|
||||
|
||||
public enum class NavbarPlacement(private val prefix: String) {
|
||||
Default(""), FixedTop("fixed-top"), FixedBottom("fixed-bottom"), StickyTop("sticky-top");
|
||||
|
||||
override fun toString(): String = prefix
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import org.jetbrains.compose.web.attributes.ButtonType
|
||||
import org.jetbrains.compose.web.attributes.type
|
||||
import org.jetbrains.compose.web.dom.Button
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
/**
|
||||
* State for [Offcanvas] composable component. Provides functions to call
|
||||
* into Bootstrap's Javascript to show/hide the Offcanvas element.
|
||||
*
|
||||
* @param confirmStateChange Optional callback invoked to signal a state change.
|
||||
*/
|
||||
public class OffcanvasState(public val confirmStateChange: (Boolean) -> Unit = {}) {
|
||||
internal var bsOffcanvas: Offcanvas? = null
|
||||
private var _visible by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* The current visibility state.
|
||||
*/
|
||||
public val visible: Boolean
|
||||
get() {
|
||||
return _visible
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the Offcanvas component.
|
||||
*/
|
||||
public fun show() {
|
||||
_visible = true
|
||||
confirmStateChange(_visible)
|
||||
bsOffcanvas?.apply { show() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the Offcanvas component.
|
||||
*/
|
||||
public fun hide() {
|
||||
_visible = false
|
||||
confirmStateChange(_visible)
|
||||
bsOffcanvas?.apply { hide() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Offcanvas component.
|
||||
*
|
||||
* @param placement The location for the Offcanvas component.
|
||||
* @param offcanvasState Control for showing/hiding the Offcanvas component.
|
||||
* @param headerContent Composable to render content for the header. A close button will always
|
||||
* be provided on the right.
|
||||
* @param showHeaderCloseButton If true the default Bootstrap close button will be shown on the right side of the
|
||||
* header. If false, the user should provide their own control for closing the Offcanvas component.
|
||||
* @param bodyContent Composable to render content for the body of the Offcanvas component.
|
||||
*/
|
||||
@Composable
|
||||
public fun Offcanvas(
|
||||
placement: OffcanvasPlacement,
|
||||
breakpoint: Breakpoint? = null,
|
||||
offcanvasState: OffcanvasState = remember { OffcanvasState() },
|
||||
headerContent: ContentBuilder<HTMLDivElement>? = null,
|
||||
showHeaderCloseButton: Boolean = true,
|
||||
bodyContent: ContentBuilder<HTMLDivElement>? = null
|
||||
) {
|
||||
Style
|
||||
needsJS
|
||||
Div(attrs = {
|
||||
if (breakpoint == null) {
|
||||
classes("offcanvas")
|
||||
} else {
|
||||
classes("offcanvas-$breakpoint")
|
||||
}
|
||||
classes(placement.toString())
|
||||
tabIndex(-1)
|
||||
}) {
|
||||
DisposableEffect(true) {
|
||||
val htmlDivElement = scopeElement
|
||||
offcanvasState.bsOffcanvas = Offcanvas(htmlDivElement)
|
||||
|
||||
// synchronize state with the offcanvas visibility
|
||||
htmlDivElement.addEventListener("hidden.bs.offcanvas", callback = {
|
||||
offcanvasState.hide()
|
||||
})
|
||||
htmlDivElement.addEventListener("shown.bs.offcanvas", callback = {
|
||||
offcanvasState.show()
|
||||
})
|
||||
|
||||
// Set initial state
|
||||
offcanvasState.bsOffcanvas?.apply {
|
||||
if (offcanvasState.visible) {
|
||||
show()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
onDispose {
|
||||
offcanvasState.bsOffcanvas?.hide()
|
||||
offcanvasState.bsOffcanvas = null
|
||||
}
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
classes("offcanvas-header")
|
||||
}) {
|
||||
headerContent?.invoke(this)
|
||||
if (showHeaderCloseButton) {
|
||||
Button(attrs = {
|
||||
type(ButtonType.Button)
|
||||
classes("btn-close", "text-reset")
|
||||
attr("aria-label", "Close")
|
||||
onClick {
|
||||
offcanvasState.bsOffcanvas?.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Div(attrs = { classes("offcanvas-body") }, content = bodyContent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to specify the location of the Offcanvas component.
|
||||
*/
|
||||
public enum class OffcanvasPlacement(private val value: String) {
|
||||
START("start"),
|
||||
END("end"),
|
||||
TOP("top"),
|
||||
BOTTOM("bottom");
|
||||
|
||||
public override fun toString(): String {
|
||||
return "offcanvas-$value"
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.HTMLLIElement
|
||||
import org.w3c.dom.HTMLUListElement
|
||||
|
||||
@Composable
|
||||
public fun Pagination(
|
||||
size: PaginationSize = PaginationSize.Normal,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLUListElement>? = null,
|
||||
content: @Composable PaginationScope.() -> Unit
|
||||
) {
|
||||
Style
|
||||
val style = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Ul(attrs = {
|
||||
classes("pagination")
|
||||
classes(size.toString())
|
||||
if (style != null) {
|
||||
classes(classes = style)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
PaginationScope().content()
|
||||
}
|
||||
}
|
||||
|
||||
public enum class PaginationSize(private val classes: String) {
|
||||
Normal(""), Small("pagination-sm"), Large("pagination-lg");
|
||||
|
||||
override fun toString(): String = classes
|
||||
}
|
||||
|
||||
public class PaginationScope {
|
||||
@Composable
|
||||
public fun PageItem(active: Boolean = false, disabled: Boolean = false, content: ContentBuilder<HTMLLIElement>) {
|
||||
Style
|
||||
Li(attrs = {
|
||||
classes("page-item")
|
||||
if (active) {
|
||||
classes(BSClasses.active)
|
||||
}
|
||||
if (disabled) {
|
||||
classes(BSClasses.disabled)
|
||||
}
|
||||
}) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun PageLink(title: String, onClick: () -> Unit) {
|
||||
Style
|
||||
A(attrs = {
|
||||
classes("page-link")
|
||||
onClick {
|
||||
onClick()
|
||||
}
|
||||
}) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
This implementation is based on https://github.com/hfhbd/bootstrap-compose, which is distributed under Apache 2.0 license.
|
@ -0,0 +1,69 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.InputType
|
||||
import org.jetbrains.compose.web.attributes.disabled
|
||||
import org.jetbrains.compose.web.attributes.forId
|
||||
import org.jetbrains.compose.web.attributes.name
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.jetbrains.compose.web.dom.Input
|
||||
import org.jetbrains.compose.web.dom.Label
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
|
||||
@Composable
|
||||
public fun RadioGroup(content: @Composable RadioGroupScope.() -> Unit) {
|
||||
val groupName = remember { "_${uuid4()}" }
|
||||
val context = RadioGroupScope(groupName)
|
||||
context.content()
|
||||
}
|
||||
|
||||
public class RadioGroupScope(private val name: String) {
|
||||
@Composable
|
||||
public fun Radio(
|
||||
label: String,
|
||||
checked: Boolean = false,
|
||||
disabled: Boolean = false,
|
||||
inline: Boolean = false,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
onClick: (Boolean) -> Unit,
|
||||
) {
|
||||
val id = remember { "_${uuid4()}" }
|
||||
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
classes(BSClasses.formCheck)
|
||||
if (inline) {
|
||||
classes(BSClasses.formCheckInline)
|
||||
}
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
}) {
|
||||
Input(attrs = {
|
||||
classes(BSClasses.formCheckInput)
|
||||
id(id)
|
||||
checked(checked)
|
||||
|
||||
onInput { event ->
|
||||
onClick(event.value)
|
||||
}
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
name(name)
|
||||
}, type = InputType.Radio)
|
||||
Label(attrs = {
|
||||
classes(BSClasses.formCheckLabel)
|
||||
forId(id)
|
||||
}) {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.disabled
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.RangeInput
|
||||
import org.jetbrains.compose.web.events.SyntheticInputEvent
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
|
||||
@Composable
|
||||
public fun Range(
|
||||
value: Number,
|
||||
min: Number? = null,
|
||||
max: Number? = null,
|
||||
step: Number = 1,
|
||||
disabled: Boolean = false,
|
||||
id: String = remember { "_${uuid4()}" },
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLInputElement>? = null,
|
||||
onInput: (SyntheticInputEvent<Number?, HTMLInputElement>) -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
RangeInput(value, min, max, step) {
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
onInput {
|
||||
onInput(it)
|
||||
}
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
id(id)
|
||||
classes(BSClasses.formRange)
|
||||
attrs?.invoke(this)
|
||||
}
|
||||
}
|
94
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Row.kt
Normal file
94
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Row.kt
Normal file
@ -0,0 +1,94 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
@Composable
|
||||
public fun Row(
|
||||
styling: (@Composable RowStyling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLDivElement>? = null,
|
||||
content: ContentBuilder<HTMLDivElement>
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
RowStyling(it).generate()
|
||||
}
|
||||
|
||||
Div(attrs = {
|
||||
classes("row")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}, content = content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun RowStyling(styling: @Composable RowStyling.() -> Unit): RowStyling = RowStyling().apply { styling() }
|
||||
|
||||
public class RowStyling : Styling() {
|
||||
public val Gutters: Gutters = Gutters()
|
||||
|
||||
@Composable
|
||||
override fun generate(): List<String> {
|
||||
return super.generate() + Gutters.generate()
|
||||
}
|
||||
}
|
||||
|
||||
public class Gutters {
|
||||
public operator fun invoke(f: Gutters.() -> Unit) {
|
||||
this.f()
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun generate(): List<String> {
|
||||
val classes: MutableList<String> = mutableListOf()
|
||||
|
||||
_spec?.let {
|
||||
classes += it.generateClassStrings()
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
public enum class GutterSize(private val value: String) {
|
||||
None("0"),
|
||||
ExtraSmall("1"),
|
||||
Small("2"),
|
||||
Medium("3"),
|
||||
Large("4"),
|
||||
ExtraLarge("5");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Direction(private val value: String) {
|
||||
Horizontal("x"),
|
||||
Vertical("y"),
|
||||
HorizontalAndVertical("");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public class GutterSpec(private val direction: Direction) {
|
||||
public var size: GutterSize = GutterSize.Small
|
||||
public var breakpoint: Breakpoint? = null
|
||||
|
||||
internal fun generateClassStrings(): String {
|
||||
return "g$direction-" + (breakpoint?.let { "$it-" } ?: "") + size.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private var _spec: GutterSpec? = null
|
||||
|
||||
public operator fun Direction.invoke(spec: GutterSpec.() -> Unit) {
|
||||
_spec = GutterSpec(this).apply(spec)
|
||||
}
|
||||
}
|
105
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Select.kt
Normal file
105
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Select.kt
Normal file
@ -0,0 +1,105 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.*
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Select
|
||||
import org.w3c.dom.HTMLOptionElement
|
||||
import org.w3c.dom.HTMLSelectElement
|
||||
|
||||
@Composable
|
||||
public fun Select(
|
||||
multiple: Boolean = false,
|
||||
size: SelectSize = SelectSize.Default,
|
||||
rows: Int? = null,
|
||||
disabled: Boolean = false,
|
||||
autocomplete: AutoComplete = AutoComplete.off,
|
||||
id: String = remember { "_${uuid4()}" },
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLSelectElement>? = null,
|
||||
onChange: (List<String>) -> Unit,
|
||||
content: @Composable SelectContext.() -> Unit,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
Select(attrs = {
|
||||
id(id)
|
||||
classes(BSClasses.formSelect)
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
if (multiple) {
|
||||
multiple()
|
||||
}
|
||||
when (size) {
|
||||
SelectSize.Small -> classes(BSClasses.formSelectSmall)
|
||||
SelectSize.Large -> classes(BSClasses.formSelectLarge)
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
rows?.let {
|
||||
size(it)
|
||||
}
|
||||
if (disabled) {
|
||||
disabled()
|
||||
}
|
||||
autoComplete(autocomplete)
|
||||
attrs?.invoke(this)
|
||||
onChange { event ->
|
||||
val selectElement = event.nativeEvent.target as HTMLSelectElement
|
||||
val options = selectElement.selectedOptions
|
||||
val results: MutableList<String> = mutableListOf()
|
||||
for (i in 0..options.length) {
|
||||
options.item(i)?.let {
|
||||
results += (it as HTMLOptionElement).value
|
||||
}
|
||||
}
|
||||
onChange(results)
|
||||
}
|
||||
}) {
|
||||
val scope = SelectContext()
|
||||
scope.content()
|
||||
}
|
||||
}
|
||||
|
||||
public class SelectContext {
|
||||
@Composable
|
||||
public fun Option(
|
||||
value: String,
|
||||
selected: Boolean? = null,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLOptionElement>? = null,
|
||||
content: ContentBuilder<HTMLOptionElement>? = null,
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
|
||||
org.jetbrains.compose.web.dom.Option(
|
||||
value = value,
|
||||
attrs = {
|
||||
if (selected == true) {
|
||||
selected()
|
||||
}
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum class SelectSize {
|
||||
Small,
|
||||
Default,
|
||||
Large
|
||||
}
|
557
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Styling.kt
Normal file
557
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Styling.kt
Normal file
@ -0,0 +1,557 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@DslMarker
|
||||
public annotation class StylingMarker
|
||||
|
||||
/**
|
||||
* Primary entrypoint for the Styling DSL. Component authors may create a subclass to add additional styling
|
||||
* capabilities to their component, including the dynamic generation of classes by using Compose-web style sheet
|
||||
* builders.
|
||||
*/
|
||||
@StylingMarker
|
||||
public open class Styling {
|
||||
private val generators: MutableList<@Composable () -> List<String>> = mutableListOf()
|
||||
public val Margins: SpacingSpecs = SpacingSpecs("m")
|
||||
public val Padding: SpacingSpecs = SpacingSpecs("p")
|
||||
public val Borders: BorderSpec = BorderSpec()
|
||||
public val Background: Background = Background()
|
||||
public val Text: Text = Text()
|
||||
public val Layout: Layout = Layout()
|
||||
|
||||
/**
|
||||
* This function will generate a list of css class names that should be applied to a component in order
|
||||
* to implement the styling features specified by the dsl. Subclasses should invoke this method and add
|
||||
* any additional class names to the returned list. This method is Composable so additional style tags can
|
||||
* be composed if new css class definitions are required.
|
||||
*
|
||||
* @return An array of css class names that are to be applied to the target component.
|
||||
*/
|
||||
@Composable
|
||||
public open fun generate(): List<String> {
|
||||
val classes: MutableList<String> = mutableListOf()
|
||||
for (gen in generators) {
|
||||
classes += gen()
|
||||
}
|
||||
|
||||
classes += Margins.generateClassStrings() +
|
||||
Padding.generateClassStrings() +
|
||||
Borders.generateClassStrings() +
|
||||
Background.generateClassStrings() +
|
||||
Text.generateClassStrings() +
|
||||
Layout.generateClassStrings()
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a styling generator. The provided function is a Composable that may create new styles and classes,
|
||||
* and returns an array of classnames that are to be applied to the target component.
|
||||
*/
|
||||
public fun registerGenerator(block: @Composable () -> List<String>) {
|
||||
generators += block
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun Styling(styling: @Composable Styling.() -> Unit): Styling = Styling().apply { styling() }
|
||||
|
||||
@StylingMarker
|
||||
public class SpacingSpecs(private val property: String) {
|
||||
public operator fun invoke(f: SpacingSpecs.() -> Unit) {
|
||||
this.f()
|
||||
}
|
||||
|
||||
public enum class Sides(private val value: String) {
|
||||
All(""),
|
||||
Top("t"),
|
||||
Bottom("b"),
|
||||
Start("s"),
|
||||
End("e"),
|
||||
Vertical("y"),
|
||||
Horizontal("x");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class SpacingSize(private val value: String) {
|
||||
None("0"),
|
||||
ExtraSmall("1"),
|
||||
Small("2"),
|
||||
Medium("3"),
|
||||
Large("4"),
|
||||
ExtraLarge("5"),
|
||||
Auto("auto");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public class SideSpec(private val sides: List<Sides>) {
|
||||
public var size: SpacingSize = SpacingSize.Small
|
||||
public var breakpoint: Breakpoint? = null
|
||||
|
||||
internal fun generateClassStrings(property: String): List<String> {
|
||||
return sides.map { side ->
|
||||
"$property$side-" + (breakpoint?.let { "$it-" } ?: "") + "$size"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _specs: MutableList<SideSpec> = mutableListOf()
|
||||
|
||||
public operator fun Sides.plus(side: Sides): List<Sides> {
|
||||
return listOf(this, side)
|
||||
}
|
||||
|
||||
public operator fun Sides.invoke(spec: SideSpec.() -> Unit) {
|
||||
_specs += SideSpec(listOf(this)).apply(spec)
|
||||
}
|
||||
|
||||
public operator fun List<Sides>.invoke(spec: SideSpec.() -> Unit) {
|
||||
_specs += SideSpec(this).apply(spec)
|
||||
}
|
||||
|
||||
internal fun generateClassStrings(): List<String> {
|
||||
return _specs.flatMap { spec ->
|
||||
spec.generateClassStrings(property)
|
||||
}
|
||||
}
|
||||
|
||||
public val All: Sides = Sides.All
|
||||
public val Top: Sides = Sides.Top
|
||||
public val Bottom: Sides = Sides.Bottom
|
||||
public val Start: Sides = Sides.Start
|
||||
public val End: Sides = Sides.End
|
||||
public val Vertical: Sides = Sides.Vertical
|
||||
public val Horizontal: Sides = Sides.Horizontal
|
||||
}
|
||||
|
||||
@StylingMarker
|
||||
public class BorderSpec {
|
||||
public operator fun invoke(f: BorderSpec.() -> Unit) {
|
||||
this.f()
|
||||
}
|
||||
|
||||
public enum class Sides(private val value: String) {
|
||||
All("border"),
|
||||
Top("border-top"),
|
||||
End("border-end"),
|
||||
Bottom("border-bottom"),
|
||||
Start("border-start");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class BorderWidth(private val value: String) {
|
||||
Thinner("border-1"),
|
||||
Thin("border-2"),
|
||||
Medium("border-3"),
|
||||
Thick("border-4"),
|
||||
Thicker("border-5");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public val All: Sides = Sides.All
|
||||
public val Top: Sides = Sides.Top
|
||||
public val End: Sides = Sides.End
|
||||
public val Bottom: Sides = Sides.Bottom
|
||||
public val Start: Sides = Sides.Start
|
||||
|
||||
public operator fun Sides.plus(side: Sides): List<Sides> {
|
||||
return listOf(this, side)
|
||||
}
|
||||
|
||||
public class SideSpec(private val sides: List<Sides>) {
|
||||
public var width: BorderWidth? = null
|
||||
public var color: Color? = null
|
||||
|
||||
internal fun generateClassString(): List<String> {
|
||||
val classList: MutableList<String> = mutableListOf()
|
||||
sides.map {
|
||||
classList += it.toString()
|
||||
}
|
||||
|
||||
return classList
|
||||
}
|
||||
}
|
||||
|
||||
public enum class BorderRadius(private val value: String) {
|
||||
All("rounded"),
|
||||
Top("rounded-top"),
|
||||
End("rounded-end"),
|
||||
Bottom("rounded-bottom"),
|
||||
Start("rounded-start"),
|
||||
Circle("rounded-circle"),
|
||||
Pill("rounded-pill");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class RadiusSize(private val value: String) {
|
||||
Small("rounded-1"),
|
||||
Medium("rounded-2"),
|
||||
Large("rounded-3");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
private var sideSpecs: SideSpec? = null
|
||||
|
||||
public operator fun Sides.invoke(spec: SideSpec.() -> Unit) {
|
||||
sideSpecs = SideSpec(listOf(this)).apply(spec)
|
||||
}
|
||||
|
||||
public operator fun List<Sides>.invoke(spec: SideSpec.() -> Unit) {
|
||||
sideSpecs = SideSpec(this).apply(spec)
|
||||
}
|
||||
|
||||
private var radiusType: BorderRadius? = null
|
||||
private var radiusSize: RadiusSize? = null
|
||||
|
||||
public fun radius(type: BorderRadius, size: RadiusSize? = null) {
|
||||
radiusType = type
|
||||
radiusSize = size
|
||||
}
|
||||
|
||||
internal fun generateClassStrings(): List<String> {
|
||||
val classList: MutableList<String> = mutableListOf()
|
||||
|
||||
sideSpecs?.let { sideSpecs ->
|
||||
classList += sideSpecs.generateClassString()
|
||||
|
||||
sideSpecs.color?.let { color ->
|
||||
classList += "border-$color"
|
||||
}
|
||||
|
||||
sideSpecs.width?.let { width ->
|
||||
classList += width.toString()
|
||||
}
|
||||
}
|
||||
|
||||
classList += listOfNotNull(radiusType, radiusSize).map {
|
||||
it.toString()
|
||||
}
|
||||
|
||||
return classList
|
||||
}
|
||||
}
|
||||
|
||||
@StylingMarker
|
||||
public class Background {
|
||||
public var color: Color? = null
|
||||
public var gradient: Boolean = false
|
||||
|
||||
public operator fun invoke(f: Background.() -> Unit) {
|
||||
this.f()
|
||||
}
|
||||
|
||||
internal fun generateClassStrings(): List<String> {
|
||||
val classes: MutableList<String> = mutableListOf()
|
||||
|
||||
color?.let { color ->
|
||||
classes += color.background()
|
||||
if (gradient) {
|
||||
classes += "bg-gradient"
|
||||
}
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
}
|
||||
|
||||
@StylingMarker
|
||||
public class Text {
|
||||
public var color: Color? = null
|
||||
private val alignments: MutableList<AlignSpec> = mutableListOf()
|
||||
public var wrap: Wraps? = null
|
||||
public var wordBreak: Boolean = false
|
||||
public var transform: Transform? = null
|
||||
public var size: Int? = null
|
||||
public var weight: Weight? = null
|
||||
public var style: Style? = null
|
||||
public var lineHeight: LineHeight? = null
|
||||
public var monospace: Boolean = false
|
||||
public var reset: Boolean = false
|
||||
public var muted: Boolean = false
|
||||
public var decoration: Decoration? = null
|
||||
|
||||
public operator fun invoke(f: Text.() -> Unit) {
|
||||
this.f()
|
||||
}
|
||||
|
||||
public enum class Alignment(private val value: String) {
|
||||
Start("start"),
|
||||
Center("center"),
|
||||
End("end");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Wraps(private val value: String) {
|
||||
Wrap("text-wrap"),
|
||||
NoWrap("text-nowrap"),
|
||||
Truncate("text-truncate");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Transform(private val value: String) {
|
||||
Lowercase("text-lowercase"),
|
||||
Uppercase("text-uppercase"),
|
||||
Capitalized("text-capitalize");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Weight(private val value: String) {
|
||||
Bold("fw-bold"),
|
||||
Bolder("fw-bolder"),
|
||||
Normal("fw-normal"),
|
||||
Light("fw-light"),
|
||||
Lighter("fw-lighter");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Style(private val value: String) {
|
||||
Italic("fst-italic"),
|
||||
Normal("fst-normal");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class LineHeight(private val value: String) {
|
||||
Smallest("lh-1"),
|
||||
Small("lh-sm"),
|
||||
Base("lh-base"),
|
||||
Large("lh-large");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Decoration(private val value: String) {
|
||||
Underline("text-decoration-underline"),
|
||||
LineThrough("text-decoration-line-through"),
|
||||
None("text-decoration-none");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public data class AlignSpec(private val alignment: Alignment, private var breakpoint: Breakpoint? = null) {
|
||||
internal fun className(): String {
|
||||
return breakpoint?.let {
|
||||
"text-$it-$alignment"
|
||||
} ?: "text-$alignment"
|
||||
}
|
||||
}
|
||||
|
||||
public fun align(alignment: Alignment, breakpoint: Breakpoint? = null) {
|
||||
alignments += AlignSpec(alignment, breakpoint)
|
||||
}
|
||||
|
||||
internal fun generateClassStrings(): List<String> {
|
||||
val classes: MutableList<String> = mutableListOf()
|
||||
|
||||
color?.let { color ->
|
||||
classes += color.text()
|
||||
}
|
||||
|
||||
alignments.forEach { spec ->
|
||||
classes += spec.className()
|
||||
}
|
||||
|
||||
if (wordBreak) {
|
||||
classes += "text-break"
|
||||
}
|
||||
|
||||
size?.let {
|
||||
classes += "fs-${it.coerceIn(1, 6)}"
|
||||
}
|
||||
|
||||
if (monospace) {
|
||||
classes += "font-monospace"
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
classes += "text-reset"
|
||||
}
|
||||
|
||||
if (muted) {
|
||||
classes += "text-muted"
|
||||
}
|
||||
|
||||
decoration?.let {
|
||||
classes += it.toString()
|
||||
}
|
||||
|
||||
classes += listOfNotNull(wrap, transform, weight, style, lineHeight).map {
|
||||
it.toString()
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
}
|
||||
|
||||
@StylingMarker
|
||||
public class Layout {
|
||||
private val displaySpecs: MutableList<DisplaySpec> = mutableListOf()
|
||||
private val floatSpecs: MutableList<FloatSpec> = mutableListOf()
|
||||
public var overflow: Overflow? = null
|
||||
public var width: Width? = null
|
||||
public var height: Height? = null
|
||||
public var verticalAlignment: VerticalAlignment? = null
|
||||
public var horizontalAlignment: HorizontalAlignment? = null
|
||||
public var visible: Boolean? = null
|
||||
|
||||
public operator fun invoke(f: Layout.() -> Unit) {
|
||||
this.f()
|
||||
}
|
||||
|
||||
public enum class Display(private val value: String) {
|
||||
None("none"),
|
||||
Inline("inline"),
|
||||
InlineBlock("inline-block"),
|
||||
Block("block"),
|
||||
Grid("grid"),
|
||||
Flex("flex"),
|
||||
InlineFlex("inline-flex");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Float(private val value: String) {
|
||||
Start("start"),
|
||||
End("end"),
|
||||
None("none");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Overflow(private val value: String) {
|
||||
Auto("overflow-auto"),
|
||||
Hidden("overflow-hidden"),
|
||||
Visible("overflow-visible"),
|
||||
Scroll("overflow-scroll");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Width(private val value: String) {
|
||||
Quarter("w-25"),
|
||||
Half("w-50"),
|
||||
ThreeQuarters("w-75"),
|
||||
Full("w-100"),
|
||||
Auto("w-auto"),
|
||||
View("vw-100");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class Height(private val value: String) {
|
||||
Quarter("h-25"),
|
||||
Half("h-50"),
|
||||
ThreeQuarters("h-75"),
|
||||
Full("h-100"),
|
||||
Auto("h-auto"),
|
||||
View("vh-100");
|
||||
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public enum class HorizontalAlignment(private val classInfix: String) {
|
||||
Start("start"),
|
||||
Center("center"),
|
||||
End("end"),
|
||||
Around("around"),
|
||||
Between("between"),
|
||||
Evenly("evenly");
|
||||
|
||||
override fun toString(): String = "justify-content-$classInfix"
|
||||
}
|
||||
|
||||
public enum class VerticalAlignment(private val value: String) {
|
||||
Baseline("align-baseline"),
|
||||
Top("align-top"),
|
||||
Middle("align-middle"),
|
||||
Bottom("align-bottom"),
|
||||
TextTop("align-text-top"),
|
||||
TextBottom("align-text-bottom");
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
private data class DisplaySpec(val display: Display, val breakpoint: Breakpoint?)
|
||||
private data class FloatSpec(val float: Float, val breakpoint: Breakpoint?)
|
||||
|
||||
public fun display(display: Display, breakpoint: Breakpoint? = null) {
|
||||
displaySpecs += DisplaySpec(display, breakpoint)
|
||||
}
|
||||
|
||||
public fun float(float: Float, breakpoint: Breakpoint? = null) {
|
||||
floatSpecs += FloatSpec(float, breakpoint)
|
||||
}
|
||||
|
||||
internal fun generateClassStrings(): List<String> {
|
||||
val classes: MutableList<String> = mutableListOf()
|
||||
|
||||
displaySpecs.forEach { displaySpec ->
|
||||
classes += "d-" + (displaySpec.breakpoint?.let { "$it-" } ?: "") + "${displaySpec.display}"
|
||||
}
|
||||
|
||||
floatSpecs.forEach { floatSpec ->
|
||||
classes += "float-" + (floatSpec.breakpoint?.let { "$it-" } ?: "") + "${floatSpec.float}"
|
||||
}
|
||||
|
||||
visible?.let { visible ->
|
||||
classes += if (visible) {
|
||||
"visible"
|
||||
} else {
|
||||
"invisible"
|
||||
}
|
||||
}
|
||||
|
||||
classes += listOfNotNull(overflow, width, height, verticalAlignment, horizontalAlignment).map {
|
||||
it.toString()
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
}
|
449
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Table.kt
Normal file
449
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Table.kt
Normal file
@ -0,0 +1,449 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import org.jetbrains.compose.web.attributes.Scope
|
||||
import org.jetbrains.compose.web.attributes.scope
|
||||
import org.jetbrains.compose.web.css.*
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
import org.w3c.dom.HTMLTableCaptionElement
|
||||
import org.w3c.dom.HTMLTableCellElement
|
||||
import org.w3c.dom.HTMLTableElement
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.chunked
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.forEachIndexed
|
||||
import kotlin.collections.lastIndex
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.mapIndexed
|
||||
import kotlin.collections.mutableListOf
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
public typealias CurrentPage<T> = Table.Pagination.Page<T>
|
||||
public typealias PageControl<T> = @Composable ElementScope<HTMLDivElement>.(
|
||||
List<Table.Pagination.Page<T>>,
|
||||
CurrentPage<T>,
|
||||
(Int) -> Unit
|
||||
) -> Unit
|
||||
|
||||
public object Table {
|
||||
public data class Column(
|
||||
val title: String,
|
||||
val scope: Scope?,
|
||||
val header: Header,
|
||||
val cell: Cell,
|
||||
val footer: Footer?
|
||||
)
|
||||
|
||||
public data class Cell internal constructor(
|
||||
public val color: Color? = null,
|
||||
val scope: Scope?,
|
||||
val verticalAlignment: Layout.VerticalAlignment?,
|
||||
val content: ContentBuilder<HTMLTableCellElement>
|
||||
)
|
||||
|
||||
public data class Footer internal constructor(
|
||||
public val color: Color? = null,
|
||||
val content: @Composable ElementScope<HTMLTableCellElement>.(List<Cell>) -> Unit
|
||||
)
|
||||
|
||||
public data class Header(
|
||||
val attrs: AttrBuilderContext<HTMLTableCellElement>? = null,
|
||||
var content: ContentBuilder<HTMLTableCellElement>? = null
|
||||
) {
|
||||
public constructor(
|
||||
color: Color,
|
||||
) : this(attrs = { classes("table-$color") })
|
||||
|
||||
public constructor(
|
||||
color: Color,
|
||||
content: ContentBuilder<HTMLTableCellElement>? = null
|
||||
) : this(attrs = { classes("table-$color") }, content)
|
||||
}
|
||||
|
||||
public data class Row(val cells: List<Cell>, public val key: Any?, val color: Color? = null)
|
||||
|
||||
public class Builder internal constructor() {
|
||||
|
||||
private val values = mutableListOf<Column>()
|
||||
|
||||
public var rowColor: Color? = null
|
||||
|
||||
internal fun build(): Pair<List<Column>, Color?> = values to rowColor
|
||||
|
||||
public fun column(
|
||||
title: String,
|
||||
scope: Scope? = null,
|
||||
header: Header? = null,
|
||||
footer: Footer? = null,
|
||||
cellColor: Color? = null,
|
||||
verticalAlignment: Layout.VerticalAlignment? = null,
|
||||
cell: ContentBuilder<HTMLTableCellElement>
|
||||
) {
|
||||
val titledHeader = when {
|
||||
header != null && header.content == null -> {
|
||||
header.content = { Text(title) }
|
||||
header
|
||||
}
|
||||
header == null -> {
|
||||
Header(attrs = null) { Text(title) }
|
||||
}
|
||||
else -> header
|
||||
}
|
||||
|
||||
values.add(
|
||||
Column(
|
||||
title = title,
|
||||
scope = scope,
|
||||
header = titledHeader,
|
||||
footer = footer,
|
||||
cell = Cell(cellColor, scope, verticalAlignment, cell)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public class FixedHeaderProperty(public val style: StyleScope.() -> Unit) {
|
||||
public constructor(topSize: CSSLengthOrPercentageValue, zIndex: ZIndex) : this({
|
||||
top(topSize)
|
||||
property("z-index", zIndex.unsafeCast<String>())
|
||||
})
|
||||
}
|
||||
|
||||
public interface Pagination<T> {
|
||||
public enum class Position {
|
||||
Top, Bottom
|
||||
}
|
||||
|
||||
public val position: Position?
|
||||
public val pages: State<List<Page<T>>>
|
||||
|
||||
public val numberOfButtons: Int
|
||||
public val entriesPerPageLimit: State<Int>?
|
||||
public val actionNavigateBack: ((CurrentPage<T>, Page<T>) -> Unit)?
|
||||
public val actionNavigateForward: ((CurrentPage<T>, Page<T>) -> Unit)?
|
||||
|
||||
public val startPageIndex: Int
|
||||
|
||||
public data class Page<T>(val index: Int, val items: List<T>, val numberOfPages: Int)
|
||||
|
||||
public val control: PageControl<T>
|
||||
|
||||
public fun defaultControl(): PageControl<T> = { pages, currentPage, goTo ->
|
||||
Column {
|
||||
Pagination(size = PaginationSize.Small) {
|
||||
PageItem(disabled = currentPage.index == 0) {
|
||||
PageLink("<") {
|
||||
val previousIndex = currentPage.index - 1
|
||||
actionNavigateBack?.invoke(currentPage, pages[previousIndex])
|
||||
goTo(previousIndex)
|
||||
}
|
||||
}
|
||||
|
||||
val buttons = tableCalcButtons(
|
||||
index = currentPage.index,
|
||||
pages = pages.size,
|
||||
numberOfButtons = numberOfButtons
|
||||
)
|
||||
|
||||
for (index in buttons) {
|
||||
if (index == currentPage.index) {
|
||||
PageItem(active = true, disabled = false) {
|
||||
PageLink("${index + 1}") { }
|
||||
}
|
||||
} else {
|
||||
PageItem {
|
||||
PageLink("${index + 1}") {
|
||||
if (index < currentPage.index) {
|
||||
actionNavigateBack?.invoke(currentPage, pages[index])
|
||||
goTo(index)
|
||||
} else {
|
||||
actionNavigateForward?.invoke(currentPage, pages[index])
|
||||
goTo(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PageItem(disabled = currentPage.index == pages.lastIndex) {
|
||||
PageLink(">") {
|
||||
val nextIndex = currentPage.index + 1
|
||||
actionNavigateForward?.invoke(currentPage, pages[nextIndex])
|
||||
goTo(nextIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (entriesPerPageLimit is MutableState<Int>) {
|
||||
val initLimit = remember { entriesPerPageLimit!!.value }
|
||||
Column(styling = {
|
||||
Margins {
|
||||
End {
|
||||
size = SpacingSpecs.SpacingSize.Auto
|
||||
}
|
||||
}
|
||||
}, auto = true) {
|
||||
DropDown("# ${entriesPerPageLimit!!.value}", size = ButtonSize.Small) {
|
||||
List(4) {
|
||||
initLimit * (it + 1)
|
||||
}.forEach {
|
||||
this.Button("$it") {
|
||||
(entriesPerPageLimit as MutableState<Int>).value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class OffsetPagination<T>(
|
||||
data: List<T>,
|
||||
public override val entriesPerPageLimit: State<Int>?,
|
||||
public override val startPageIndex: Int = 0,
|
||||
public override val position: Pagination.Position? = Pagination.Position.Bottom,
|
||||
public override val numberOfButtons: Int = 5,
|
||||
public override val actionNavigateBack: ((CurrentPage<T>, Pagination.Page<T>) -> Unit)? = null,
|
||||
public override val actionNavigateForward: ((CurrentPage<T>, Pagination.Page<T>) -> Unit)? = null,
|
||||
) : Pagination<T> {
|
||||
|
||||
override val pages: State<List<Pagination.Page<T>>> =
|
||||
data.chunked(entriesPerPageLimit?.value ?: data.size).let {
|
||||
mutableStateOf(
|
||||
it.mapIndexed { index, data ->
|
||||
Pagination.Page(index, data, it.size)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override var control: PageControl<T> = defaultControl()
|
||||
|
||||
public constructor(
|
||||
data: List<T>,
|
||||
entriesPerPageLimit: State<Int>,
|
||||
startPageIndex: Int = 0,
|
||||
position: Pagination.Position = Pagination.Position.Bottom,
|
||||
numberOfButtons: Int = 5,
|
||||
actionNavigateBack: ((CurrentPage<T>, Pagination.Page<T>) -> Unit)? = null,
|
||||
actionNavigateForward: ((CurrentPage<T>, Pagination.Page<T>) -> Unit)? = null,
|
||||
control: PageControl<T>
|
||||
) : this(
|
||||
data,
|
||||
entriesPerPageLimit,
|
||||
startPageIndex,
|
||||
position,
|
||||
numberOfButtons,
|
||||
actionNavigateBack,
|
||||
actionNavigateForward
|
||||
) {
|
||||
this.control = control
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun tableCalcButtons(index: Int, pages: Int, numberOfButtons: Int): IntRange {
|
||||
if (pages <= numberOfButtons) {
|
||||
return 0 until pages
|
||||
}
|
||||
val nr = min(pages, numberOfButtons)
|
||||
val max = pages - 1
|
||||
return when (index) {
|
||||
0 -> 0 until nr
|
||||
(pages - 1) -> (max((pages - nr), 0)) until pages
|
||||
else -> {
|
||||
val half = nr / 2
|
||||
val lower = max(index - half, 0)
|
||||
val upper = (min(index + half, max))
|
||||
if (lower == 0) {
|
||||
0 until nr
|
||||
} else if (upper == max) {
|
||||
val newLower = pages - numberOfButtons
|
||||
newLower until pages
|
||||
} else {
|
||||
lower..upper
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
public fun <T> Table(
|
||||
data: List<T>,
|
||||
key: ((T) -> Any)? = null,
|
||||
color: Color? = null,
|
||||
stripedRows: Boolean = false,
|
||||
stripedColumns: Boolean = false,
|
||||
hover: Boolean = false,
|
||||
borderless: Boolean = false,
|
||||
small: Boolean = false,
|
||||
fixedHeader: Table.FixedHeaderProperty? = null,
|
||||
caption: ContentBuilder<HTMLTableCaptionElement>? = null,
|
||||
captionTop: Boolean = false,
|
||||
attrs: AttrBuilderContext<HTMLTableElement>? = null,
|
||||
map: Table.Builder.(Int, T) -> Unit
|
||||
) {
|
||||
Table(
|
||||
pagination = Table.OffsetPagination(data, null, position = null),
|
||||
key,
|
||||
color,
|
||||
stripedRows = stripedRows,
|
||||
stripedColumns = stripedColumns,
|
||||
hover,
|
||||
borderless,
|
||||
small,
|
||||
fixedHeader,
|
||||
caption,
|
||||
captionTop,
|
||||
attrs,
|
||||
map
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun <T> Table(
|
||||
pagination: Table.Pagination<T>,
|
||||
key: ((T) -> Any)? = null,
|
||||
color: Color? = null,
|
||||
stripedRows: Boolean = false,
|
||||
stripedColumns: Boolean = false,
|
||||
hover: Boolean = false,
|
||||
borderless: Boolean = false,
|
||||
small: Boolean = false,
|
||||
fixedHeader: Table.FixedHeaderProperty? = null,
|
||||
caption: ContentBuilder<HTMLTableCaptionElement>? = null,
|
||||
captionTop: Boolean = false,
|
||||
attrs: AttrBuilderContext<HTMLTableElement>? = null,
|
||||
map: Table.Builder.(Int, T) -> Unit
|
||||
) {
|
||||
Style
|
||||
val headers = mutableMapOf<String, Table.Header>()
|
||||
val _footers = mutableListOf<Table.Footer>()
|
||||
|
||||
val pages by pagination.pages
|
||||
|
||||
var currentIndex by remember { mutableStateOf(pagination.startPageIndex) }
|
||||
val currentPage = pages[min(currentIndex, pages.lastIndex)]
|
||||
val baseIndex = currentPage.index * (pagination.entriesPerPageLimit?.value ?: 0)
|
||||
|
||||
val rows = currentPage.items.mapIndexed { itemIndexOfPage, item ->
|
||||
val (columns, rowColor) = Table.Builder().apply {
|
||||
val index = baseIndex + itemIndexOfPage
|
||||
map(index, item)
|
||||
}.build()
|
||||
val cells = columns.map {
|
||||
headers[it.title] = it.header
|
||||
if (it.footer != null) {
|
||||
_footers.add(it.footer)
|
||||
}
|
||||
it.cell
|
||||
}
|
||||
Table.Row(color = rowColor, cells = cells, key = key?.invoke(item))
|
||||
}
|
||||
check(rows.all { it.cells.size == headers.size })
|
||||
val footers = _footers.takeUnless { it.isEmpty() }
|
||||
if (footers != null) {
|
||||
check(rows.all { it.cells.size == footers.size })
|
||||
}
|
||||
|
||||
if (pagination.position == Table.Pagination.Position.Top) {
|
||||
Row {
|
||||
val control = pagination.control
|
||||
control(pages, currentPage) {
|
||||
currentIndex = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Table(attrs = {
|
||||
classes("table")
|
||||
if (captionTop) {
|
||||
classes("caption-top")
|
||||
}
|
||||
if (small) {
|
||||
classes("table-sm")
|
||||
}
|
||||
color?.let { classes("table-$it") }
|
||||
if (hover) {
|
||||
classes("table-hover")
|
||||
}
|
||||
if (stripedRows) {
|
||||
classes("table-striped")
|
||||
}
|
||||
if (stripedColumns) {
|
||||
classes("table-striped-columns")
|
||||
}
|
||||
if (borderless) {
|
||||
classes("table-borderless")
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
if (caption != null) {
|
||||
Caption(content = caption)
|
||||
}
|
||||
Thead {
|
||||
Tr {
|
||||
headers.forEach { (_, header) ->
|
||||
Th(attrs = {
|
||||
scope(Scope.Col)
|
||||
header.attrs?.invoke(this)
|
||||
if (fixedHeader != null) {
|
||||
classes("sticky-top")
|
||||
style {
|
||||
fixedHeader.style(this)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
header.content?.invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Tbody {
|
||||
for (row in rows) {
|
||||
key(row.key) {
|
||||
Tr(attrs = {
|
||||
row.color?.let { classes("table-$it") }
|
||||
}) {
|
||||
for (cell in row.cells) {
|
||||
Td(attrs = {
|
||||
cell.color?.let { classes("table-$it") }
|
||||
cell.scope?.let { scope(it) }
|
||||
cell.verticalAlignment?.let { classes(it.toString()) }
|
||||
}) {
|
||||
cell.content(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (footers != null) {
|
||||
Tfoot {
|
||||
Tr {
|
||||
footers.forEachIndexed { index, cell ->
|
||||
Td(attrs = {
|
||||
cell.color?.let { classes("table-$it") }
|
||||
}) {
|
||||
cell.content(this, rows[index].cells)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pagination.position == Table.Pagination.Position.Bottom) {
|
||||
Row {
|
||||
val control = pagination.control
|
||||
control(pages, currentPage) {
|
||||
currentIndex = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
184
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Toasts.kt
Normal file
184
visionforge-compose-html/src/jsMain/kotlin/bootstrap/Toasts.kt
Normal file
@ -0,0 +1,184 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.AttrsScope
|
||||
import org.jetbrains.compose.web.attributes.ButtonType
|
||||
import org.jetbrains.compose.web.attributes.type
|
||||
import org.jetbrains.compose.web.dom.Button
|
||||
import org.jetbrains.compose.web.dom.ContentBuilder
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
import org.w3c.dom.HTMLButtonElement
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
public fun ToastContainer(
|
||||
toastContainerState: ToastContainerState,
|
||||
attrs: (AttrsScope<HTMLDivElement>.() -> Unit)? = null,
|
||||
) {
|
||||
Div(attrs = {
|
||||
classes("toast-container")
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
toastContainerState.toasts.forEach { toastItem ->
|
||||
key(toastItem) {
|
||||
Toast(toastItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Toast(message: ToastContainerState.ToastItem) {
|
||||
Style
|
||||
needsJS
|
||||
Div(
|
||||
attrs = {
|
||||
classes("toast")
|
||||
attr("role", "alert")
|
||||
attr("data-bs-delay", message.delay.inWholeMilliseconds.toString())
|
||||
attr("aria-live", "assertive")
|
||||
attr("aria-atomic", "true")
|
||||
message.toastAttrs?.invoke(this)
|
||||
}
|
||||
) {
|
||||
DisposableEffect(message) {
|
||||
val htmlDivElement = scopeElement
|
||||
val bsToast = Toast(htmlDivElement)
|
||||
htmlDivElement.addEventListener("shown.bs.toast", callback = {
|
||||
})
|
||||
htmlDivElement.addEventListener("hidden.bs.toast", callback = {
|
||||
message.remove()
|
||||
})
|
||||
bsToast.show()
|
||||
onDispose {
|
||||
bsToast.dispose()
|
||||
message.remove()
|
||||
htmlDivElement.removeEventListener("shown.bs.toast", callback = {
|
||||
})
|
||||
htmlDivElement.removeEventListener("hidden.bs.toast", callback = {
|
||||
message.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
message.header?.let { header ->
|
||||
Div(attrs = {
|
||||
classes("toast-header")
|
||||
}) {
|
||||
header()
|
||||
if (message.withDismissButton) {
|
||||
DismissButton(attrs = message.dismissButtonAttrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.header == null && message.withDismissButton) {
|
||||
Div(attrs = { classes("d-flex") }) {
|
||||
Div(attrs = { classes("toast-body") }, content = message.body)
|
||||
DismissButton(styling = {
|
||||
Margins {
|
||||
End {
|
||||
size = SpacingSpecs.SpacingSize.Small
|
||||
}
|
||||
All {
|
||||
size = SpacingSpecs.SpacingSize.Auto
|
||||
}
|
||||
}
|
||||
}) {
|
||||
message.dismissButtonAttrs?.invoke(this)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Div(attrs = { classes("toast-body") }, content = message.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DismissButton(
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: (AttrsScope<HTMLButtonElement>.() -> Unit)? = null,
|
||||
) {
|
||||
val style = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
Button(attrs = {
|
||||
type(ButtonType.Button)
|
||||
classes("btn-close")
|
||||
attr("data-bs-dismiss", "toast")
|
||||
attr("aria-label", "close")
|
||||
if (style != null) {
|
||||
classes(classes = style)
|
||||
}
|
||||
attrs?.invoke(this)
|
||||
})
|
||||
}
|
||||
|
||||
public class ToastContainerState {
|
||||
internal val toasts = mutableStateListOf<ToastItem>()
|
||||
|
||||
public fun showToast(toastMessage: String) {
|
||||
showToast(body = {
|
||||
Text(toastMessage)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a new Toast.
|
||||
* @param header Composable to generate the header content
|
||||
* @param body Composable to generate the body content
|
||||
* @param withDismissButton Set to True to show a default dismiss button icon, and false to not.
|
||||
* You can also choose to include your own dismiss button in the [header] or [body] content
|
||||
* instead of in addition to the default.
|
||||
*
|
||||
* @return A function the caller can invoke to dismiss the toast, for example, if you provide your own
|
||||
* dismiss button in the header or body.
|
||||
*/
|
||||
public fun showToast(
|
||||
withDismissButton: Boolean = true,
|
||||
delay: Duration = 5.seconds,
|
||||
toastAttrs: (AttrsScope<HTMLDivElement>.() -> Unit)? = null,
|
||||
dismissButtonAttrs: (AttrsScope<HTMLButtonElement>.() -> Unit)? = null,
|
||||
header: ContentBuilder<HTMLDivElement>? = null,
|
||||
body: ContentBuilder<HTMLDivElement>,
|
||||
): () -> Unit {
|
||||
val uuid = uuid4()
|
||||
val toastItem = ToastItem(
|
||||
uuid,
|
||||
delay = delay,
|
||||
withDismissButton,
|
||||
toastAttrs,
|
||||
dismissButtonAttrs,
|
||||
header = header,
|
||||
body = body
|
||||
) {
|
||||
removeToast(uuid)
|
||||
}
|
||||
toasts.add(toastItem)
|
||||
return { removeToast(uuid) }
|
||||
}
|
||||
|
||||
private fun removeToast(uuid: Uuid) {
|
||||
toasts.removeAll {
|
||||
it.uuid == uuid
|
||||
}
|
||||
}
|
||||
|
||||
internal data class ToastItem(
|
||||
val uuid: Uuid,
|
||||
val delay: Duration,
|
||||
val withDismissButton: Boolean,
|
||||
val toastAttrs: (AttrsScope<HTMLDivElement>.() -> Unit)?,
|
||||
val dismissButtonAttrs: (AttrsScope<HTMLButtonElement>.() -> Unit)?,
|
||||
val header: ContentBuilder<HTMLDivElement>?,
|
||||
val body: ContentBuilder<HTMLDivElement>?,
|
||||
val remove: () -> Unit,
|
||||
)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package bootstrap
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.Button
|
||||
import org.jetbrains.compose.web.dom.Span
|
||||
import org.w3c.dom.HTMLButtonElement
|
||||
|
||||
@Composable
|
||||
public fun Toggler(
|
||||
target: String,
|
||||
controls: String,
|
||||
styling: (Styling.() -> Unit)? = null,
|
||||
attrs: AttrBuilderContext<HTMLButtonElement>? = null
|
||||
) {
|
||||
Style
|
||||
val classes = styling?.let {
|
||||
Styling().apply(it).generate()
|
||||
}
|
||||
Button(attrs = {
|
||||
classes("navbar-toggler")
|
||||
if (classes != null) {
|
||||
classes(classes = classes)
|
||||
}
|
||||
attr("data-bs-toggle", "collapse")
|
||||
attr("data-bs-target", "#$target")
|
||||
attr("aria-controls", controls)
|
||||
attr("aria-expanded", "false")
|
||||
attr("aria-label", "Toggle navigation")
|
||||
attrs?.invoke(this)
|
||||
}) {
|
||||
Span(attrs = { classes("navbar-toggler-icon") })
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
@file:Suppress(
|
||||
"NAME_CONTAINS_ILLEGAL_CHARS",
|
||||
"NESTED_CLASS_IN_EXTERNAL_INTERFACE",
|
||||
"NOTHING_TO_INLINE",
|
||||
)
|
||||
|
||||
package bootstrap
|
||||
|
||||
// language=JavaScript
|
||||
@JsName("""(/*union*/{auto: 'auto'}/*union*/)""")
|
||||
public sealed external interface ZIndex {
|
||||
public companion object {
|
||||
public val auto: ZIndex
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun ZIndex(value: Int): ZIndex =
|
||||
value.unsafeCast<ZIndex>()
|
@ -0,0 +1,24 @@
|
||||
@file:JsModule("bootstrap")
|
||||
@file:JsNonModule
|
||||
|
||||
package bootstrap
|
||||
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
|
||||
internal external class Modal(element: HTMLDivElement) {
|
||||
internal fun show()
|
||||
internal fun hide()
|
||||
internal fun dispose()
|
||||
}
|
||||
|
||||
internal external class Toast(element: HTMLDivElement) {
|
||||
internal fun show()
|
||||
internal fun dispose()
|
||||
}
|
||||
|
||||
internal external class Offcanvas(element: HTMLDivElement) {
|
||||
internal fun show()
|
||||
internal fun hide()
|
||||
}
|
||||
|
||||
internal external val needsJS: dynamic
|
@ -0,0 +1,6 @@
|
||||
@file:JsModule("@popperjs/core")
|
||||
@file:JsNonModule
|
||||
|
||||
package bootstrap
|
||||
|
||||
internal external val needsPopper: dynamic
|
@ -0,0 +1,5 @@
|
||||
package bootstrap
|
||||
|
||||
@JsModule("bootstrap/scss/bootstrap.scss")
|
||||
@JsNonModule
|
||||
internal external val Style: dynamic
|
@ -1,15 +1,18 @@
|
||||
package space.kscience.visionforge.html
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import app.softwork.bootstrapcompose.CloseButton
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.web.attributes.ButtonType
|
||||
import org.jetbrains.compose.web.attributes.disabled
|
||||
import org.jetbrains.compose.web.attributes.type
|
||||
import org.jetbrains.compose.web.css.AlignItems
|
||||
import org.jetbrains.compose.web.css.alignItems
|
||||
import org.jetbrains.compose.web.css.px
|
||||
import org.jetbrains.compose.web.css.width
|
||||
import org.jetbrains.compose.web.dom.Button
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.jetbrains.compose.web.dom.Span
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
@ -120,10 +123,16 @@ public fun PropertyEditor(
|
||||
|
||||
}
|
||||
if (!name.isEmpty()) {
|
||||
CloseButton(editorPropertyState != EditorPropertyState.Defined) {
|
||||
rootMeta.remove(name)
|
||||
update()
|
||||
}
|
||||
Button(attrs = {
|
||||
type(ButtonType.Button)
|
||||
if(editorPropertyState != EditorPropertyState.Defined) disabled()
|
||||
classes("btn-close")
|
||||
onClick {
|
||||
rootMeta.remove(name)
|
||||
update()
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (expanded) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
package space.kscience.visionforge.html
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import app.softwork.bootstrapcompose.Card
|
||||
import app.softwork.bootstrapcompose.NavbarLink
|
||||
import app.softwork.bootstrapcompose.Styling
|
||||
import bootstrap.Card
|
||||
import bootstrap.NavbarLink
|
||||
import bootstrap.Styling
|
||||
import org.jetbrains.compose.web.css.overflowY
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
|
@ -3,8 +3,7 @@
|
||||
package space.kscience.visionforge.html
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.uuid.UUID
|
||||
import kotlinx.uuid.generateUUID
|
||||
import com.benasher44.uuid.uuid4
|
||||
import org.jetbrains.compose.web.attributes.*
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.HTMLOptionElement
|
||||
@ -45,7 +44,7 @@ public fun BooleanValueChooser(
|
||||
value: Value?,
|
||||
onValueChange: (Value?) -> Unit,
|
||||
) {
|
||||
val uid = remember { "checkbox[${UUID.generateUUID().toString(false)}]" }
|
||||
val uid = remember { "checkbox[${uuid4()}]" }
|
||||
var innerValue by remember(value, descriptor) {
|
||||
mutableStateOf(
|
||||
value?.boolean ?: descriptor?.defaultValue?.boolean
|
||||
|
@ -19,7 +19,7 @@ public interface VisionGroup : Vision {
|
||||
|
||||
override fun update(change: VisionChange) {
|
||||
change.children?.forEach { (name, change) ->
|
||||
if (change.vision != null || change.vision == NullVision) {
|
||||
if (change.vision != null) {
|
||||
error("VisionGroup is read-only")
|
||||
} else {
|
||||
children.getChild(name)?.update(change)
|
||||
|
@ -23,8 +23,8 @@ import space.kscience.visionforge.useProperty
|
||||
|
||||
public actual class MarkupPlugin : VisionPlugin(), ElementVisionRenderer {
|
||||
public val visionClient: JsVisionClient by require(JsVisionClient)
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
override val visionSerializersModule: SerializersModule get() = markupSerializersModule
|
||||
actual override val tag: PluginTag get() = Companion.tag
|
||||
actual override val visionSerializersModule: SerializersModule get() = markupSerializersModule
|
||||
|
||||
override fun rateVision(vision: Vision): Int = when (vision) {
|
||||
is VisionOfMarkup -> ElementVisionRenderer.DEFAULT_RATING
|
||||
@ -57,9 +57,9 @@ public actual class MarkupPlugin : VisionPlugin(), ElementVisionRenderer {
|
||||
}
|
||||
|
||||
public actual companion object : PluginFactory<MarkupPlugin> {
|
||||
override val tag: PluginTag = PluginTag("vision.markup.js", PluginTag.DATAFORGE_GROUP)
|
||||
actual override val tag: PluginTag = PluginTag("vision.markup.js", PluginTag.DATAFORGE_GROUP)
|
||||
|
||||
override fun build(context: Context, meta: Meta): MarkupPlugin = MarkupPlugin()
|
||||
actual override fun build(context: Context, meta: Meta): MarkupPlugin = MarkupPlugin()
|
||||
|
||||
}
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
package space.kscience.visionforge.solid.three.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import app.softwork.bootstrapcompose.Button
|
||||
import app.softwork.bootstrapcompose.Color.Info
|
||||
import app.softwork.bootstrapcompose.Column
|
||||
import app.softwork.bootstrapcompose.Layout.Height
|
||||
import app.softwork.bootstrapcompose.Layout.Width
|
||||
import bootstrap.Button
|
||||
import bootstrap.Color
|
||||
import bootstrap.Column
|
||||
import org.jetbrains.compose.web.dom.Hr
|
||||
import org.w3c.files.Blob
|
||||
import org.w3c.files.BlobPropertyBag
|
||||
@ -22,7 +20,7 @@ internal fun CanvasControls(
|
||||
) {
|
||||
Column {
|
||||
vision?.let { vision ->
|
||||
Button("Export", color = Info, styling = { Layout.width = Width.Full }) {
|
||||
Button("Export", color = Color.Info, styling = { Layout.width = bootstrap.Layout.Width.Full }) {
|
||||
val json = vision.encodeToString()
|
||||
|
||||
val fileSaver = kotlinext.js.require<dynamic>("file-saver")
|
||||
@ -51,7 +49,7 @@ public fun ThreeControls(
|
||||
) {
|
||||
Tabs(
|
||||
styling = {
|
||||
Layout.height = Height.Full
|
||||
Layout.height = bootstrap.Layout.Height.Full
|
||||
}
|
||||
) {
|
||||
vision?.let { vision ->
|
||||
|
@ -1,11 +1,9 @@
|
||||
package space.kscience.visionforge.solid.three.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import app.softwork.bootstrapcompose.Card
|
||||
import app.softwork.bootstrapcompose.Column
|
||||
import app.softwork.bootstrapcompose.Layout.Height
|
||||
import app.softwork.bootstrapcompose.Layout.Width
|
||||
import app.softwork.bootstrapcompose.Row
|
||||
import bootstrap.Card
|
||||
import bootstrap.Column
|
||||
import bootstrap.Row
|
||||
import kotlinx.dom.clear
|
||||
import org.jetbrains.compose.web.css.*
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
@ -83,15 +81,15 @@ public fun ThreeView(
|
||||
Row(
|
||||
styling = {
|
||||
Layout {
|
||||
width = Width.Full
|
||||
height = Height.Full
|
||||
width = bootstrap.Layout.Width.Full
|
||||
height = bootstrap.Layout.Height.Full
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
styling = {
|
||||
Layout {
|
||||
height = Height.Full
|
||||
height = bootstrap.Layout.Height.Full
|
||||
}
|
||||
},
|
||||
attrs = {
|
||||
@ -166,7 +164,7 @@ public fun ThreeView(
|
||||
auto = true,
|
||||
styling = {
|
||||
Layout {
|
||||
height = Height.Full
|
||||
height = bootstrap.Layout.Height.Full
|
||||
}
|
||||
},
|
||||
attrs = {
|
||||
|
Loading…
Reference in New Issue
Block a user