From 9e243a469ae6544e8cf523ad09b959f541c3f565 Mon Sep 17 00:00:00 2001 From: Robert Jaros Date: Wed, 11 Mar 2020 16:18:17 +0100 Subject: Upgrade to Kotlin 1.3.70 + other dependencies (Coroutinse, Serialization, Spring Boot) Major refactoring of build architecture. --- src/main/kotlin/pl/treksoft/kvision/KVManager.kt | 6 +- src/main/kotlin/pl/treksoft/kvision/core/Widget.kt | 2 +- src/main/kotlin/pl/treksoft/kvision/form/Form.kt | 58 +-- .../kotlin/pl/treksoft/kvision/form/FormPanel.kt | 12 +- .../kotlin/pl/treksoft/kvision/html/Template.kt | 2 +- .../kotlin/pl/treksoft/kvision/rest/RestClient.kt | 20 +- src/main/kotlin/pl/treksoft/kvision/types/Date.kt | 19 +- .../kotlin/pl/treksoft/kvision/types/Decimal.kt | 24 -- src/main/kotlin/pl/treksoft/kvision/utils/JSON.kt | 4 +- .../test/pl/treksoft/kvision/form/FormSpec.kt | 1 - .../test/pl/treksoft/kvision/html/ImageSpec.kt | 2 +- src/test/resources/css/style.css | 445 +++++++++++++++++++++ src/test/resources/img/placeholder.png | 0 13 files changed, 492 insertions(+), 103 deletions(-) delete mode 100644 src/main/kotlin/pl/treksoft/kvision/types/Decimal.kt create mode 100644 src/test/resources/css/style.css create mode 100644 src/test/resources/img/placeholder.png (limited to 'src') 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 = setOf()) : StyledComponent(), Component * } * } */ - @Suppress("UNCHECKED_CAST") + @Suppress("UNCHECKED_CAST", "UnsafeCastFromDynamic") open fun setEventListener(block: SnOn.() -> Unit): Int { val handlerCounter = listenerCounter++ val blockAsWidget = block as SnOn.() -> 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. @@ -45,27 +48,6 @@ internal data class FieldParams( val validator: ((F) -> Boolean?)? = null ) -/** - * A wrapper for a Map with a custom containsKey method implementation. - * Used with kotlinx.serialization Mapper. - */ -private class FormMapWrapper(private val map: Map) : Map { - 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 get() = map.keys - override val values: Collection get() = map.values - override val entries: Set> get() = map.entries -} - /** * The form definition class. Can be used directly or indirectly inside a [FormPanel]. * @@ -89,36 +71,26 @@ class Form( 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)?.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)?.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( } 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( } companion object { - @UseExperimental(ImplicitReflectionSerializer::class) + @OptIn(ImplicitReflectionSerializer::class) inline fun create( panel: FormPanel? = null, customSerializers: Map, 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( * 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( set(value) { form.validatorMessage = value } + /** * Validation function. */ @@ -178,6 +187,7 @@ open class FormPanel( */ @Suppress("LeakingThis") val form = Form(this, serializer, customSerializers) + /** * @suppress * Internal property. @@ -459,7 +469,7 @@ open class FormPanel( companion object { - @UseExperimental(ImplicitReflectionSerializer::class) + @OptIn(ImplicitReflectionSerializer::class) inline fun 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 Template.setData(obj: K, serializer: SerializationStrategy) { /** * Extension function to set serializable object as a template data. */ -@UseExperimental(ImplicitReflectionSerializer::class) +@OptIn(ImplicitReflectionSerializer::class) inline fun 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 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 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 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 call( url: String, serializer: SerializationStrategy, @@ -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 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 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 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 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 request( url: String, serializer: SerializationStrategy, @@ -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 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 { - 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 { } } - 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 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 -- cgit