aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/KVManager.kt6
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Widget.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/Form.kt58
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt12
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/Template.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/rest/RestClient.kt20
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/types/Date.kt19
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt24
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/utils/JSON.kt4
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/form/FormSpec.kt1
-rw-r--r--src/test/kotlin/test/pl/treksoft/kvision/html/ImageSpec.kt2
-rw-r--r--src/test/resources/css/style.css445
-rw-r--r--src/test/resources/img/placeholder.png0
13 files changed, 492 insertions, 103 deletions
diff --git a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
index 0ba694eb..1cd41e7d 100644
--- a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
@@ -46,15 +46,15 @@ external fun require(name: String): dynamic
object KVManager {
init {
try {
- require("kvision-bootstrap-css").pl.treksoft.kvision.KVManagerBootstrapCss
+ require("kvision-kvision-bootstrap-css").pl.treksoft.kvision.KVManagerBootstrapCss
} catch (e: Throwable) {
}
try {
- require("kvision-bootstrap").pl.treksoft.kvision.KVManagerBootstrap
+ require("kvision-kvision-bootstrap").pl.treksoft.kvision.KVManagerBootstrap
} catch (e: Throwable) {
}
try {
- require("kvision-fontawesome").pl.treksoft.kvision.KVManagerFontAwesome
+ require("kvision-kvision-fontawesome").pl.treksoft.kvision.KVManagerFontAwesome
} catch (e: Throwable) {
}
require("./css/style.css")
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt
index 918d0ad0..c03316ab 100644
--- a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt
@@ -355,7 +355,7 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent(), Component
* }
* }
*/
- @Suppress("UNCHECKED_CAST")
+ @Suppress("UNCHECKED_CAST", "UnsafeCastFromDynamic")
open fun <T : Widget> setEventListener(block: SnOn<T>.() -> Unit): Int {
val handlerCounter = listenerCounter++
val blockAsWidget = block as SnOn<Widget>.() -> Unit
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/Form.kt b/src/main/kotlin/pl/treksoft/kvision/form/Form.kt
index 5e49e1b4..3eb94f64 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/Form.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/Form.kt
@@ -21,19 +21,22 @@
*/
package pl.treksoft.kvision.form
+import kotlinx.serialization.DynamicObjectParser
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.KSerializer
-import kotlinx.serialization.Mapper
+import kotlinx.serialization.builtins.list
import kotlinx.serialization.modules.serializersModuleOf
import kotlinx.serialization.serializer
import pl.treksoft.kvision.i18n.I18n.trans
import pl.treksoft.kvision.types.DateSerializer
import pl.treksoft.kvision.types.KFile
import pl.treksoft.kvision.types.toStringF
+import pl.treksoft.kvision.utils.JSON.toObj
import kotlin.js.Date
import kotlin.js.Json
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
+import kotlin.js.JSON as NativeJSON
/**
* Internal data class containing form field parameters.
@@ -46,27 +49,6 @@ internal data class FieldParams<in F : FormControl>(
)
/**
- * A wrapper for a Map with a custom containsKey method implementation.
- * Used with kotlinx.serialization Mapper.
- */
-private class FormMapWrapper<out V>(private val map: Map<String, V>) : Map<String, V> {
- override fun equals(other: Any?): Boolean = map == other
- override fun hashCode(): Int = map.hashCode()
- override fun toString(): String = map.toString()
- override val size: Int get() = map.size
- override fun isEmpty(): Boolean = map.isEmpty()
- override fun containsKey(key: String): Boolean =
- if (key.indexOf('.') != -1) map.containsKey(key) else
- !(map.containsKey("$key.time") || map.containsKey("$key.size"))
-
- override fun containsValue(value: @UnsafeVariance V): Boolean = map.containsValue(value)
- override fun get(key: String): V? = map[key]
- override val keys: Set<String> get() = map.keys
- override val values: Collection<V> get() = map.values
- override val entries: Set<Map.Entry<String, V>> get() = map.entries
-}
-
-/**
* The form definition class. Can be used directly or indirectly inside a [FormPanel].
*
* @constructor Creates a form with a given modelFactory function
@@ -89,36 +71,26 @@ class Form<K : Any>(
init {
modelFactory = {
- val map = it.flatMap { entry ->
- when (entry.value) {
+ val json = js("{}")
+ it.forEach { (key, value) ->
+ val v = when (value) {
is Date -> {
- listOf(entry.key to (entry.value as? Date)?.toStringF())
+ value.toStringF()
}
is List<*> -> {
@Suppress("UNCHECKED_CAST")
- (entry.value as? List<KFile>)?.let { list ->
- listOf(entry.key to entry.value, "${entry.key}.size" to list.size) +
- list.mapIndexed { index, kFile ->
- listOf(
- "${entry.key}.$index.name" to kFile.name,
- "${entry.key}.$index.size" to kFile.size,
- "${entry.key}.$index.content" to kFile.content
- )
- }.flatten()
- } ?: listOf()
+ ((value as? List<KFile>)?.toObj(KFile.serializer().list))
}
- else -> listOf(entry.key to entry.value)
+ else -> value
}
- }.toMap()
+ json[key] = v
+ }
val serializersModule = if (customSerializers == null) {
serializersModuleOf(Date::class, DateSerializer)
} else {
serializersModuleOf(customSerializers + (Date::class to DateSerializer))
}
- Mapper(context = serializersModule).unmapNullable(
- serializer,
- FormMapWrapper(map)
- )
+ DynamicObjectParser(serializersModule).parse(json, serializer)
}
}
@@ -315,7 +287,7 @@ class Form<K : Any>(
} else {
serializersModuleOf(customSerializers + (Date::class to DateSerializer))
}
- return JSON.parse(
+ return NativeJSON.parse(
kotlinx.serialization.json.Json(context = serializersModule).stringify(
serializer,
getData()
@@ -361,7 +333,7 @@ class Form<K : Any>(
}
companion object {
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified K : Any> create(
panel: FormPanel<K>? = null,
customSerializers: Map<KClass<*>, KSerializer<*>>? = null,
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt b/src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt
index ba144137..8f918a97 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/FormPanel.kt
@@ -114,34 +114,42 @@ open class FormPanel<K : Any>(
* HTTP method.
*/
var method by refreshOnUpdate(method)
+
/**
* The URL address to send data.
*/
var action by refreshOnUpdate(action)
+
/**
* The form encoding type.
*/
var enctype by refreshOnUpdate(enctype)
+
/**
* The form name.
*/
var name: String? by refreshOnUpdate()
+
/**
* The form target.
*/
var target: FormTarget? by refreshOnUpdate()
+
/**
* Determines if the form is not validated.
*/
var novalidate: Boolean? by refreshOnUpdate()
+
/**
* Determines if the form should have autocomplete.
*/
var autocomplete: Boolean? by refreshOnUpdate()
+
/**
* Determines if the form is condensed.
*/
var condensed by refreshOnUpdate(condensed)
+
/**
* Horizontal form layout ratio.
*/
@@ -155,6 +163,7 @@ open class FormPanel<K : Any>(
set(value) {
form.validatorMessage = value
}
+
/**
* Validation function.
*/
@@ -178,6 +187,7 @@ open class FormPanel<K : Any>(
*/
@Suppress("LeakingThis")
val form = Form(this, serializer, customSerializers)
+
/**
* @suppress
* Internal property.
@@ -459,7 +469,7 @@ open class FormPanel<K : Any>(
companion object {
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified K : Any> create(
method: FormMethod? = null, action: String? = null, enctype: FormEnctype? = null,
type: FormType? = null, condensed: Boolean = false,
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/Template.kt b/src/main/kotlin/pl/treksoft/kvision/html/Template.kt
index 7b5b26a5..b9e165ef 100644
--- a/src/main/kotlin/pl/treksoft/kvision/html/Template.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/html/Template.kt
@@ -66,7 +66,7 @@ fun <K> Template.setData(obj: K, serializer: SerializationStrategy<K>) {
/**
* Extension function to set serializable object as a template data.
*/
-@UseExperimental(ImplicitReflectionSerializer::class)
+@OptIn(ImplicitReflectionSerializer::class)
inline fun <reified K : Any> Template.setData(obj: K) {
this.setData(obj, K::class.serializer())
}
diff --git a/src/main/kotlin/pl/treksoft/kvision/rest/RestClient.kt b/src/main/kotlin/pl/treksoft/kvision/rest/RestClient.kt
index b1ab97b6..9011c780 100644
--- a/src/main/kotlin/pl/treksoft/kvision/rest/RestClient.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/rest/RestClient.kt
@@ -180,7 +180,7 @@ open class RestClient {
* @param transform a function to transform the result of the call
* @return a promise of the result
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified T : Any> call(
url: String,
data: dynamic = null,
@@ -201,7 +201,7 @@ open class RestClient {
* @param beforeSend a function to set request parameters
* @return a promise of the result
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified V : Any> call(
url: String,
data: V,
@@ -230,7 +230,7 @@ open class RestClient {
* @param transform a function to transform the result of the call
* @return a promise of the result
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <T : Any, reified V : Any> call(
url: String,
data: V,
@@ -263,7 +263,7 @@ open class RestClient {
* @param transform a function to transform the result of the call
* @return a promise of the result
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified T : Any, V : Any> call(
url: String,
serializer: SerializationStrategy<V>,
@@ -295,7 +295,7 @@ open class RestClient {
* @param transform a function to transform the result of the call
* @return a promise of the result
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified T : Any, reified V : Any> call(
url: String,
data: V,
@@ -470,7 +470,7 @@ open class RestClient {
* @param transform a function to transform the result of the call
* @return a promise of the response
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified T : Any> request(
url: String,
data: dynamic = null,
@@ -491,7 +491,7 @@ open class RestClient {
* @param beforeSend a function to set request parameters
* @return a promise of the response
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified V : Any> request(
url: String,
data: V,
@@ -520,7 +520,7 @@ open class RestClient {
* @param transform a function to transform the result of the call
* @return a promise of the response
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <T : Any, reified V : Any> request(
url: String,
data: V,
@@ -553,7 +553,7 @@ open class RestClient {
* @param transform a function to transform the result of the call
* @return a promise of the response
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified T : Any, V : Any> request(
url: String,
serializer: SerializationStrategy<V>,
@@ -585,7 +585,7 @@ open class RestClient {
* @param transform a function to transform the result of the call
* @return a promise of the response
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified T : Any, reified V : Any> request(
url: String,
data: V,
diff --git a/src/main/kotlin/pl/treksoft/kvision/types/Date.kt b/src/main/kotlin/pl/treksoft/kvision/types/Date.kt
index 889d26fc..74bcc28d 100644
--- a/src/main/kotlin/pl/treksoft/kvision/types/Date.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/types/Date.kt
@@ -25,22 +25,9 @@ import kotlinx.serialization.Decoder
import kotlinx.serialization.Encoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialDescriptor
-import kotlinx.serialization.internal.SerialClassDescImpl
import pl.treksoft.kvision.KVManager
import kotlin.js.Date
-const val KV_DEFAULT_DATE_FORMAT = "YYYY-MM-DD HH:mm:ss"
-
-actual typealias LocalDateTime = Date
-
-actual typealias LocalDate = Date
-
-actual typealias LocalTime = Date
-
-actual typealias OffsetDateTime = Date
-
-actual typealias OffsetTime = Date
-
/**
* Extension function to convert String to Date with a given date format.
* @param format date/time format
@@ -63,7 +50,7 @@ fun Date.toStringF(format: String = KV_DEFAULT_DATE_FORMAT): String {
}
object DateSerializer : KSerializer<Date> {
- override val descriptor: SerialDescriptor = SerialClassDescImpl("kotlin.js.Date")
+ override val descriptor: SerialDescriptor = SerialDescriptor("kotlin.js.Date")
override fun deserialize(decoder: Decoder): Date {
val str = decoder.decodeString()
@@ -74,7 +61,7 @@ object DateSerializer : KSerializer<Date> {
}
}
- override fun serialize(encoder: Encoder, obj: Date) {
- encoder.encodeString(obj.toStringF())
+ override fun serialize(encoder: Encoder, value: Date) {
+ encoder.encodeString(value.toStringF())
}
}
diff --git a/src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt b/src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt
deleted file mode 100644
index d1c0366e..00000000
--- a/src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (c) 2017-present Robert Jaros
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package pl.treksoft.kvision.types
-
-actual typealias Decimal = Double
diff --git a/src/main/kotlin/pl/treksoft/kvision/utils/JSON.kt b/src/main/kotlin/pl/treksoft/kvision/utils/JSON.kt
index 8dd8a504..4d951028 100644
--- a/src/main/kotlin/pl/treksoft/kvision/utils/JSON.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/utils/JSON.kt
@@ -38,14 +38,14 @@ object JSON {
val plain = Json(context = serializersModuleOf(Date::class, DateSerializer))
val nonstrict = Json(
- configuration = JsonConfiguration.Stable.copy(strictMode = false),
+ configuration = JsonConfiguration.Stable.copy(ignoreUnknownKeys = true),
context = serializersModuleOf(Date::class, DateSerializer)
)
/**
* An extension function to convert Serializable object to JS dynamic object
*/
- @UseExperimental(ImplicitReflectionSerializer::class)
+ @OptIn(ImplicitReflectionSerializer::class)
inline fun <reified T : Any> T.toObj(): dynamic {
return this.toObj(T::class.serializer())
}
diff --git a/src/test/kotlin/test/pl/treksoft/kvision/form/FormSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/form/FormSpec.kt
index de875c67..5bc4cc95 100644
--- a/src/test/kotlin/test/pl/treksoft/kvision/form/FormSpec.kt
+++ b/src/test/kotlin/test/pl/treksoft/kvision/form/FormSpec.kt
@@ -43,7 +43,6 @@ data class DataForm2(
val d: String? = null
)
-
@Suppress("CanBeParameter")
class FormSpec : SimpleSpec {
diff --git a/src/test/kotlin/test/pl/treksoft/kvision/html/ImageSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/html/ImageSpec.kt
index ed3a5347..a3ac9d66 100644
--- a/src/test/kotlin/test/pl/treksoft/kvision/html/ImageSpec.kt
+++ b/src/test/kotlin/test/pl/treksoft/kvision/html/ImageSpec.kt
@@ -35,7 +35,7 @@ class ImageSpec : DomSpec {
fun render() {
run {
val root = Root("test", fixed = true)
- val res = require("./img/placeholder.png")
+ val res = require("img/placeholder.png")
@Suppress("UnsafeCastFromDynamic")
val image = Image(res, "Image", true, ImageShape.ROUNDED, true)
root.add(image)
diff --git a/src/test/resources/css/style.css b/src/test/resources/css/style.css
new file mode 100644
index 00000000..cae50162
--- /dev/null
+++ b/src/test/resources/css/style.css
@@ -0,0 +1,445 @@
+.splitpanel-vertical {
+ display: flex;
+ flex-direction: row;
+ overflow: auto;
+}
+
+.splitpanel-vertical > *:first-child {
+ max-width: calc(100% - 9px);
+}
+
+.splitpanel-vertical > * {
+ flex: 0 0 auto;
+ overflow: auto;
+}
+
+.splitpanel-vertical > *:last-child {
+ flex: 1 1 auto;
+ overflow: auto;
+}
+
+.splitpanel-horizontal {
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+}
+
+.splitpanel-horizontal > *:first-child {
+ max-height: calc(100% - 9px);
+}
+
+.splitpanel-horizontal > * {
+ flex: 0 0 auto;
+ overflow: auto;
+}
+
+.splitpanel-horizontal > *:last-child {
+ flex: 1 1 auto;
+ overflow: auto;
+}
+
+.splitter-vertical {
+ flex: 0 0 auto;
+ width: 9px;
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAhCAQAAABOpSL+AAAAIklEQVR4AWMwbb/PdR+JZDD9f1/oPhI5sgVGBSruc9xHIgGdSQqqQJGkRgAAAABJRU5ErkJggg==') center center no-repeat #cecece;
+ cursor: col-resize;
+}
+
+.splitter-horizontal {
+ flex: 0 0 auto;
+ height: 9px;
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAICAQAAADdTl4aAAAAIElEQVQoz2MwrTD9TxFsZ7jPcV+IIsjFQAUw6hFqegQA+xzRHT2p7pEAAAAASUVORK5CYII=') center center no-repeat #cecece;
+ cursor: row-resize;
+}
+
+.trix-control {
+ overflow-y: auto;
+}
+
+trix-toolbar .trix-button-group {
+ margin-bottom: 3px;
+}
+
+.tabulator-row .tabulator-cell.tabulator-editing input, .tabulator-row .tabulator-cell.tabulator-editing select {
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+.tabulator-row .tabulator-cell.tabulator-editing input:focus, .tabulator-row .tabulator-cell.tabulator-editing select:focus {
+ border-color: #66afe9;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.tabulator-row .tabulator-cell.tabulator-editing {
+ border-right: 1px solid #1d68cd !important;
+ padding: 2px !important;
+}
+
+.input-group.date.is-invalid~.invalid-feedback {
+ display: block;
+}
+
+.input-group.date {
+ padding-left: 0px;
+ padding-right: 0px;
+}
+
+.bootstrap-select .dropdown-toggle.btn-default:focus {
+ outline: none !important;
+}
+
+.bootstrap-select .dropdown-toggle::after {
+ margin-left: -1em !important;
+}
+
+.select-parent.text-danger>.invalid-feedback {
+ display: block;
+}
+.select-parent.text-danger>div.form-control>button.form-control {
+ border-color: #dc3545;
+}
+
+.form-inline .bootstrap-select .form-control {
+ min-width: 200px;
+}
+
+label.required-label::after {
+ content: " *";
+ color: #dc3545;
+}
+
+.kv-spinner-btn-none .input-group-btn-vertical {
+ display: none;
+}
+
+.kv-spinner-btn-none .form-control {
+ border-radius: 4px !important;
+}
+
+.kv-spinner > span {
+ display: inline-block;
+ width: 100%;
+}
+
+.input-group.kv-spinner {
+ padding-left: 0px;
+ padding-right: 0px;
+}
+
+.kv-spinner.is-invalid~.invalid-feedback {
+ display: block;
+}
+
+.kv-radiogroup-inline label.control-label {
+ vertical-align: top;
+ margin-right: .75rem;
+ margin-bottom: 0px;
+}
+.row.kv-radiogroup-inline label.control-label {
+ margin-right: 0px;
+}
+
+.row.kv-radiogroup-inline .kv-radiogroup-container, .row.kv-radiogroup .kv-radiogroup-container {
+ margin-left: -15px;
+}
+
+.kv-radiogroup-inline .kv-radiogroup-container {
+ display: inline-flex;
+}
+
+.kv-radiogroup-container.is-invalid~.invalid-feedback {
+ display: block;
+}
+
+.form-check {
+ padding-left: 0.5rem;
+}
+
+.form-check-input.form-control-sm, .form-check-input.form-control-lg {
+ height: inherit;
+}
+
+.form-check-inline {
+ margin-left: 3px;
+}
+
+.form-check-inline.form-check {
+ padding-left: 0px;
+}
+
+.form-horizontal.container-fluid {
+ width: inherit;
+}
+
+.form-inline .form-group {
+ margin-right: 6px;
+}
+
+.form-inline .form-group .control-label {
+ margin-right: 6px;
+}
+
+.form-inline .form-check.form-group {
+ margin-left: 6px;
+}
+
+.kv-form-condensed .form-group {
+ margin-bottom: 0.5rem;
+}
+
+.kv-window.modal-content {
+ -webkit-box-shadow: 0 5px 15px rgba(0,0,0,.5);
+ box-shadow: 0 5px 15px rgba(0,0,0,.5);
+ border-radius: 0px;
+}
+
+.kv-window .modal-header {
+ height: 40px;
+ padding: 5px 15px 5px 15px;
+ align-items: center;
+}
+
+.kv-window .modal-header button.close {
+ width: 24px;
+ height: 24px;
+ margin: 0px;
+ padding: 0px;
+}
+
+.kv-window .modal-header .modal-title {
+ white-space: nowrap;
+}
+
+.kv-window .modal-header .window-icon {
+ margin-right: 6px;
+}
+
+.kv-window .kv-window-icons-container {
+ display: flex;
+}
+
+.kv-preview-thumb .btn, .kv-zoom-actions .btn, .file-zoom-dialog .floating-buttons .btn {
+ padding: 5px 8px;
+}
+
+.file-drop-zone.clickable:hover {
+ border: 1px dashed #999;
+}
+
+.file-drop-zone.clickable:focus {
+ border: 1px solid #5acde2;
+}
+
+.nav.tabs-top {
+ flex-wrap: nowrap;
+}
+
+ul.tabs-top {
+ overflow-x: auto;
+ overflow-y: hidden;
+ display: flex;
+}
+
+ul.tabs-top > li {
+ float:none;
+ flex-shrink: 0;
+}
+
+.kv-tab-close {
+ margin-left: 10px;
+ color: #000;
+ text-shadow: 0 1px 0 #fff;
+ filter: alpha(opacity=20);
+ opacity: 0.2;
+}
+
+.kv-tab-close:hover, .kv-tab-close:focus {
+ cursor: pointer;
+ filter: alpha(opacity=50);
+ opacity: 0.5;
+}
+
+select.form-control, .tabulator-row .tabulator-cell.tabulator-editing select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: transparent none no-repeat;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAKCAYAAABblxXYAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wUKFyIn4IjqJgAAAENJREFUKM/l0LERACEQQlGsiTa2px1aokGugNNAx8wfMy8AeLoBALYjaTqoKkga2+gKPgF/2Q7JkEx359oftu+C7/UBCUIcVQz0PvcAAAAASUVORK5CYII=');
+ background-position: right center;
+ cursor: pointer;
+}
+
+.abc-checkbox input[type="checkbox"]:checked+label::after,
+.abc-checkbox input[type="radio"]:checked+label::after {
+ font-family: "Font Awesome 5 Pro", "Font Awesome 5 Free";
+ content: "\f00c";
+ font-weight: 900;
+}
+
+.abc-checkbox label::before {
+ top: 0px;
+}
+.abc-checkbox label::after {
+ top: 0px;
+}
+
+.abc-radio label::before {
+ top: 0px;
+}
+
+.abc-radio label::after {
+ top: 3px;
+}
+
+.abc-checkbox.form-check-inline label::before {
+ top: 2px;
+}
+
+.abc-checkbox.form-check-inline label::after {
+ top: 2px;
+}
+
+.abc-radio.form-check-inline label::before {
+ top: 2px;
+}
+
+.abc-radio.form-check-inline label::after {
+ top: 5px;
+}
+
+.abc-checkbox label.col-form-label-lg::before {
+ top: 10px;
+}
+.abc-checkbox label.col-form-label-lg::after {
+ top: 10px;
+}
+
+.abc-radio label.col-form-label-lg::before {
+ top: 10px;
+}
+
+.abc-radio label.col-form-label-lg::after {
+ top: 13px;
+}
+
+.abc-checkbox.form-check-inline label.col-form-label-lg::before {
+ top: 15px;
+}
+
+.abc-checkbox.form-check-inline label.col-form-label-lg::after {
+ top: 15px;
+}
+
+.abc-radio.form-check-inline label.col-form-label-lg::before {
+ top: 15px;
+}
+
+.abc-radio.form-check-inline label.col-form-label-lg::after {
+ top: 18px;
+}
+
+/*!
+ * bootstrap-vertical-tabs - v1.2.2
+ * https://dbtek.github.io/bootstrap-vertical-tabs
+ * 2016-12-02
+ * Copyright (c) 2016 İsmail Demirbilek
+ * License: MIT
+ */
+.nav-tabs.tabs-left, .nav-tabs.tabs-right {
+ border-bottom: none;
+ padding-top: 2px;
+}
+.nav-tabs.tabs-left {
+ border-right: 1px solid #dee2e6;
+}
+.nav-tabs.tabs-right {
+ border-left: 1px solid #dee2e6;
+}
+.nav-tabs.tabs-left>li.nav-item, .nav-tabs.tabs-right>li.nav-item {
+ float: none;
+ margin-bottom: 2px;
+}
+.nav-tabs.tabs-left>li.nav-item {
+ margin-right: -1px;
+}
+.nav-tabs.tabs-right>li.nav-item {
+ margin-left: -1px;
+}
+.nav-tabs.tabs-left>li.nav-item>a.nav-link.active,
+.nav-tabs.tabs-left>li.nav-item>a.nav-link.active:hover,
+.nav-tabs.tabs-left>li.nav-item>a.nav-link.active:focus {
+ border-bottom-color: #dee2e6;
+ border-right-color: transparent;
+}
+.nav-tabs.tabs-right>li.nav-item>a.nav-link.active,
+.nav-tabs.tabs-right>li.nav-item>a.nav-link.active:hover,
+.nav-tabs.tabs-right>li.nav-item>a.nav-link.active:focus {
+ border-bottom: 1px solid #dee2e6;
+ border-left-color: transparent;
+}
+.nav-tabs.tabs-left>li.nav-item>a.nav-link {
+ border-radius: 4px 0 0 4px;
+ margin-right: 0;
+ display:block;
+}
+.nav-tabs.tabs-right>li.nav-item>a.nav-link {
+ border-radius: 0 4px 4px 0;
+ margin-right: 0;
+}
+
+.kv-focus {
+ border-radius: 0.25rem;
+ outline-width: 0px;
+ box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
+}
+
+.kv-focus .kv-fileinput-caption {
+ border-color: #80bdff;
+}
+
+.modal-dialog .modal-footer {
+ flex-wrap: wrap;
+}
+
+.modal-dialog .modal-footer>button {
+ margin-top: 5px;
+}
+
+.kv_fieldset {
+ border: 1px solid #dee2e6;
+ border-radius: 0.25rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.kv_fieldset legend {
+ border: 1px solid #dee2e6;
+ border-radius: 0.25rem;
+ margin-bottom: 0;
+ font-size: 1rem;
+ font-weight: bold;
+ padding: 3px 10px 3px 10px;
+ width: auto;
+}
+
+form fieldset.kv_fieldset {
+ padding-top: 5px;
+ margin-bottom: 8px;
+}
+
+form[class~="form-horizontal"] fieldset.kv_fieldset {
+ padding-left: 1.1rem;
+ padding-right: 2rem;
+ margin-right: -15px;
+ margin-left: -15px;
+}
+
+form[class~="form-horizontal"] div.form-group {
+ align-items: center;
+}
+
+ul.typeahead > li.active > a {
+ text-decoration: none;
+ background-color: #f8f9fa;
+}
diff --git a/src/test/resources/img/placeholder.png b/src/test/resources/img/placeholder.png
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/test/resources/img/placeholder.png