diff options
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/KVManager.kt | 19 | ||||
-rw-r--r-- | src/main/kotlin/pl/treksoft/kvision/window/Window.kt | 274 | ||||
-rw-r--r-- | src/main/resources/css/style.css | 9 |
3 files changed, 302 insertions, 0 deletions
diff --git a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt index 4ba518b5..d3001b1d 100644 --- a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt +++ b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt @@ -29,6 +29,7 @@ import com.github.snabbdom.datasetModule import com.github.snabbdom.eventListenersModule import com.github.snabbdom.propsModule import com.github.snabbdom.styleModule +import pl.treksoft.kvision.core.Component import kotlin.browser.document import kotlin.dom.clear @@ -102,6 +103,10 @@ internal object KVManager { require("bootstrap-touchspin/dist/jquery.bootstrap-touchspin.min.js") } catch (e: Throwable) { } + private val elementResizeEvent = try { + require("element-resize-event") + } catch (e: Throwable) { + } private val resizable = require("jquery-resizable-dom") internal val fecha = require("fecha") @@ -128,4 +133,18 @@ internal object KVManager { internal fun virtualize(html: String): VNode { return sdVirtualize(html) } + + @Suppress("UnsafeCastFromDynamic") + internal fun setResizeEvent(component: Component, callback: () -> Unit) { + component.getElement()?.let { + elementResizeEvent(it, callback) + } + } + + @Suppress("UnsafeCastFromDynamic") + internal fun clearResizeEvent(component: Component) { + component.getElement()?.let { + elementResizeEvent.unbind(it) + } + } } diff --git a/src/main/kotlin/pl/treksoft/kvision/window/Window.kt b/src/main/kotlin/pl/treksoft/kvision/window/Window.kt new file mode 100644 index 00000000..63ff26e8 --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/window/Window.kt @@ -0,0 +1,274 @@ +/* + * 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.window + +import com.github.snabbdom.VNode +import org.w3c.dom.events.Event +import org.w3c.dom.events.MouseEvent +import pl.treksoft.kvision.KVManager +import pl.treksoft.kvision.core.Component +import pl.treksoft.kvision.core.Container +import pl.treksoft.kvision.core.CssSize +import pl.treksoft.kvision.core.Overflow +import pl.treksoft.kvision.core.Position +import pl.treksoft.kvision.core.Resize +import pl.treksoft.kvision.core.UNIT +import pl.treksoft.kvision.html.TAG +import pl.treksoft.kvision.html.Tag +import pl.treksoft.kvision.modal.CloseIcon +import pl.treksoft.kvision.panel.SimplePanel +import pl.treksoft.kvision.utils.px + +internal const val DEFAULT_Z_INDEX = 1000 +internal const val WINDOW_HEADER_HEIGHT = 40 +internal const val WINDOW_CONTENT_MARGIN_BOTTOM = 11 + +/** + * Floating window container. + * + * @constructor + * @param caption window title + * @param contentWidth window content width + * @param contentHeight window content height + * @param isResizable determines if the window is resizable + * @param isDraggable determines if the window is draggable + * @param closeButton determines if Close button is visible + * @param classes a set of CSS class names + * @param init an initializer extension function + */ +open class Window( + caption: String? = null, + contentWidth: CssSize? = CssSize(0, UNIT.auto), + contentHeight: CssSize? = CssSize(0, UNIT.auto), + isResizable: Boolean = true, + isDraggable: Boolean = true, + closeButton: Boolean = false, + classes: Set<String> = setOf(), + init: (Window.() -> Unit)? = null +) : + SimplePanel(classes + setOf("modal-content", "kv-window")) { + + /** + * Window caption text. + */ + var caption + get() = captionTag.text + set(value) { + captionTag.text = value + checkHeaderVisibility() + } + /** + * Window content width. + */ + var contentWidth + get() = width + set(value) { + width = value + } + /** + * Window content height. + */ + var contentHeight + get() = content.height + set(value) { + content.height = value + } + /** + * Determines if the window is resizable. + */ + var isResizable by refreshOnUpdate(isResizable, { checkIsResizable() }) + /** + * Determines if the window is draggable. + */ + var isDraggable by refreshOnUpdate(isDraggable, { checkIsDraggable(); refresh() }) + /** + * Determines if Close button is visible. + */ + var closeButton + get() = closeIcon.visible + set(value) { + closeIcon.visible = value + checkHeaderVisibility() + } + + private val header = SimplePanel(setOf("modal-header")) + + /** + * @suppress + * Internal property. + */ + protected val content = SimplePanel().apply { + this.height = contentHeight + this.overflow = Overflow.AUTO + } + private val closeIcon = CloseIcon() + private val captionTag = Tag(TAG.H4, caption, classes = setOf("modal-title")) + + private var isResizeEvent = false + + init { + position = Position.ABSOLUTE + overflow = Overflow.HIDDEN + @Suppress("LeakingThis") + width = contentWidth + zIndex = DEFAULT_Z_INDEX + closeIcon.visible = closeButton + closeIcon.setEventListener { + click = { + hide() + } + } + header.add(closeIcon) + header.add(captionTag) + checkHeaderVisibility() + addInternal(header) + addInternal(content) + checkIsDraggable() + if (isResizable) { + resize = Resize.BOTH + content.marginBottom = WINDOW_CONTENT_MARGIN_BOTTOM.px + } + @Suppress("LeakingThis") + init?.invoke(this) + } + + private fun checkHeaderVisibility() { + if (!closeButton && caption == null && !isDraggable) { + header.hide() + } else { + header.show() + } + } + + private fun checkIsDraggable() { + var isDrag: Boolean + if (isDraggable) { + header.setEventListener<SimplePanel> { + mousedown = { e -> + isDrag = true + val dragStartX = this@Window.getElementJQuery()?.position()?.left?.toInt() ?: 0 + val dragStartY = this@Window.getElementJQuery()?.position()?.top?.toInt() ?: 0 + val dragMouseX = e.pageX + val dragMouseY = e.pageY + val moveCallback = { me: Event -> + if (isDrag) { + this@Window.left = (dragStartX + (me as MouseEvent).pageX - dragMouseX).toInt().px + this@Window.top = (dragStartY + (me).pageY - dragMouseY).toInt().px + } + } + kotlin.browser.window.addEventListener("mousemove", moveCallback) + var upCallback: ((Event) -> Unit)? = null + upCallback = { _ -> + isDrag = false + kotlin.browser.window.removeEventListener("mousemove", moveCallback) + kotlin.browser.window.removeEventListener("mouseup", upCallback) + } + kotlin.browser.window.addEventListener("mouseup", upCallback) + } + } + } else { + isDrag = false + header.removeEventListeners() + } + } + + private fun checkIsResizable() { + checkResizablEventHandler() + if (isResizable) { + resize = Resize.BOTH + val intHeight = (getElementJQuery()?.height()?.toInt() ?: 0) + 2 + this.height = (intHeight + WINDOW_CONTENT_MARGIN_BOTTOM - 1).px + content.marginBottom = WINDOW_CONTENT_MARGIN_BOTTOM.px + } else { + resize = Resize.NONE + val intHeight = (getElementJQuery()?.height()?.toInt() ?: 0) + 2 + this.height = (intHeight - WINDOW_CONTENT_MARGIN_BOTTOM - 1).px + content.marginBottom = 0.px + } + } + + private fun checkResizablEventHandler() { + if (isResizable) { + isResizeEvent = true + KVManager.setResizeEvent(this) { + val intWidth = (getElementJQuery()?.width()?.toInt() ?: 0) + 2 + val intHeight = (getElementJQuery()?.height()?.toInt() ?: 0) + 2 + width = intWidth.px + height = intHeight.px + content.width = (intWidth - 2).px + content.height = (intHeight - WINDOW_HEADER_HEIGHT - WINDOW_CONTENT_MARGIN_BOTTOM - 1 - 2).px + } + } else if (isResizeEvent) { + KVManager.clearResizeEvent(this) + } + } + + override fun add(child: Component): SimplePanel { + content.add(child) + return this + } + + override fun addAll(children: List<Component>): SimplePanel { + content.addAll(children) + return this + } + + override fun remove(child: Component): SimplePanel { + content.remove(child) + return this + } + + override fun removeAll(): SimplePanel { + content.removeAll() + return this + } + + override fun getChildren(): List<Component> { + return content.getChildren() + } + + override fun afterCreate(node: VNode) { + checkResizablEventHandler() + } + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun Container.window( + caption: String? = null, + width: CssSize? = CssSize(0, UNIT.auto), + height: CssSize? = CssSize(0, UNIT.auto), + resizable: Boolean = true, + draggable: Boolean = true, + closeButton: Boolean = false, + classes: Set<String> = setOf(), + init: (Window.() -> Unit)? = null + ): Window { + val window = Window(caption, width, height, resizable, draggable, closeButton, classes, init) + this.add(window) + return window + } + } +} diff --git a/src/main/resources/css/style.css b/src/main/resources/css/style.css index 71aeface..295cf268 100644 --- a/src/main/resources/css/style.css +++ b/src/main/resources/css/style.css @@ -118,3 +118,12 @@ trix-toolbar .trix-button-group { .kv-radio-checkbox { padding-left: 7px; } + +.kv-window { + border-radius: 0px; +} + +.kv-window .modal-header { + height: 40px; + padding: 10px 15px 5px 15px; +} |