All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.kvision.form.time.DateTimeInput.kt Maven / Gradle / Ivy

/*
 * 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 io.kvision.form.time

import io.kvision.snabbdom.VNode
import io.kvision.BootstrapDatetimeModule
import io.kvision.core.ClassSetBuilder
import io.kvision.core.Container
import io.kvision.core.bindAllJQueryListeners
import io.kvision.core.getElementJQuery
import io.kvision.core.getElementJQueryD
import io.kvision.core.removeAllJQueryListeners
import io.kvision.form.FormInput
import io.kvision.form.GenericFormComponent
import io.kvision.form.text.TextInput
import io.kvision.html.Icon
import io.kvision.html.Span
import io.kvision.html.icon
import io.kvision.i18n.I18n
import io.kvision.jquery.invoke
import io.kvision.jquery.jQuery
import io.kvision.panel.SimplePanel
import io.kvision.state.MutableState
import io.kvision.types.toDateF
import io.kvision.types.toStringF
import io.kvision.utils.obj
import kotlin.js.Date

internal const val DEFAULT_STEPPING = 5

/**
 * Basic date/time chooser component.
 *
 * @constructor
 * @param value date/time input value
 * @param format date/time format (default YYYY-MM-DD HH:mm)
 * @param className CSS class names
 * @param init an initializer extension function
 */
@Suppress("TooManyFunctions", "LeakingThis")
open class DateTimeInput(
    value: Date? = null, format: String = "YYYY-MM-DD HH:mm",
    className: String? = null,
    init: (DateTimeInput.() -> Unit)? = null
) : SimplePanel((className?.let { "$it " } ?: "") + "input-group date"), GenericFormComponent, FormInput,
    MutableState {

    protected val observers = mutableListOf<(Date?) -> Unit>()

    private var initialized = false

    val input = TextInput(value = value?.toStringF(format))
    private lateinit var icon: Icon
    private val addon = Span(className = "input-group-text datepickerbutton") {
        [email protected] = icon([email protected](format))
    }

    /**
     * Date/time input value.
     */
    override var value
        get() = input.value?.toDateF(format)
        set(value) {
            input.value = value?.toStringF(format)
            refreshState()
        }

    /**
     * Date/time format.
     */
    var format by refreshOnUpdate(format) { refreshDatePicker() }

    /**
     * The placeholder for the date/time input.
     */
    var placeholder
        get() = input.placeholder
        set(value) {
            input.placeholder = value
        }

    /**
     * The name attribute of the generated HTML input element.
     */
    override var name
        get() = input.name
        set(value) {
            input.name = value
        }

    /**
     * Determines if the field is disabled.
     */
    override var disabled
        get() = input.disabled
        set(value) {
            input.disabled = value
        }

    /**
     * Determines if the text input is automatically focused.
     */
    var autofocus
        get() = input.autofocus
        set(value) {
            input.autofocus = value
        }

    /**
     * Determines if the date/time input is read-only.
     */
    var readonly
        get() = input.readonly
        set(value) {
            input.readonly = value
        }

    /**
     * The size of the input.
     */
    override var size
        get() = input.size
        set(value) {
            input.size = value
        }

    /**
     * The validation status of the input.
     */
    override var validationStatus
        get() = input.validationStatus
        set(value) {
            input.validationStatus = value
            refresh()
        }

    /**
     * Days of the week that should be disabled. Multiple values should be comma separated.
     */
    var daysOfWeekDisabled by refreshOnUpdate(arrayOf()) { refreshDatePicker() }

    /**
     * Determines if *Clear* button should be visible.
     */
    var showClear by refreshOnUpdate(true) { refreshDatePicker() }

    /**
     * Determines if *Close* button should be visible.
     */
    var showClose by refreshOnUpdate(true) { refreshDatePicker() }

    /**
     * Determines if *Today* button should be visible.
     */
    var showTodayButton by refreshOnUpdate(true) { refreshDatePicker() }

    /**
     * The increment used to build the hour view.
     */
    var stepping by refreshOnUpdate(DEFAULT_STEPPING) { refreshDatePicker() }

    /**
     * Prevents date selection before this date.
     */
    var minDate: Date? by refreshOnUpdate { refreshDatePicker() }

    /**
     * Prevents date selection after this date.
     */
    var maxDate: Date? by refreshOnUpdate { refreshDatePicker() }

    /**
     * Shows date and time pickers side by side.
     */
    var sideBySide by refreshOnUpdate(false) { refreshDatePicker() }

    /**
     * An array of enabled dates.
     */
    var enabledDates by refreshOnUpdate(arrayOf()) { refreshDatePicker() }

    /**
     * An array of disabled dates.
     */
    var disabledDates by refreshOnUpdate(arrayOf()) { refreshDatePicker() }

    /**
     * Allow date picker for readonly component.
     */
    var ignoreReadonly by refreshOnUpdate(false) { refreshDatePicker() }

    /**
     * Show as inline.
     */
    var inline by refreshOnUpdate(false) { refreshDatePicker() }

    /**
     * Keep the popup open after selecting a date.
     */
    var keepOpen by refreshOnUpdate(false) { refreshDatePicker() }

    /**
     * Focus text input when the popup is opened.
     */
    var focusOnShow by refreshOnUpdate(true) { refreshDatePicker() }

    init {
        useSnabbdomDistinctKey()
        addPrivate(input)
        addPrivate(addon)
        init?.invoke(this)
    }

    private fun refreshState() {
        if (initialized) getElementJQueryD().data("DateTimePicker").date(value)
    }

    private fun getIconClass(format: String): String {
        return if (format.contains("YYYY") || format.contains("MM") || format.contains("DD")) {
            "fas fa-calendar-alt"
        } else {
            "fas fa-clock"
        }
    }

    override fun buildClassSet(classSetBuilder: ClassSetBuilder) {
        super.buildClassSet(classSetBuilder)
        classSetBuilder.add(validationStatus)
    }

    protected open fun refreshDatePicker() {
        if (initialized) {
            getElementJQueryD()?.data("DateTimePicker").destroy()
        }
        input.visible = !inline
        addon.visible = !inline
        initDateTimePicker()
        icon.icon = getIconClass(format)
    }

    /**
     * Open date/time chooser popup.
     */
    open fun showPopup() {
        if (initialized) getElementJQueryD()?.data("DateTimePicker").show()
    }

    /**
     * Hides date/time chooser popup.
     */
    open fun hidePopup() {
        if (initialized) getElementJQueryD()?.data("DateTimePicker").hide()
    }

    /**
     * Toggles date/time chooser popup.
     */
    open fun togglePopup() {
        if (initialized) getElementJQueryD()?.data("DateTimePicker").toggle()
    }

    @Suppress("UnsafeCastFromDynamic")
    override fun afterInsert(node: VNode) {
        this.initDateTimePicker()
        this.initEventHandlers()
        initialized = true
    }

    override fun afterDestroy() {
        if (initialized) {
            getElementJQueryD()?.data("DateTimePicker")?.destroy()
            initialized = false
        }
    }

    private fun initDateTimePicker() {
        val language = I18n.language
        val self = this
        getElementJQueryD()?.datetimepicker(obj {
            this.useCurrent = inline
            this.defaultDate = if (inline) [email protected] else undefined
            this.format = format
            this.stepping = stepping
            this.showClear = showClear
            this.showClose = showClose
            this.showTodayButton = showTodayButton
            this.sideBySide = sideBySide
            this.ignoreReadonly = ignoreReadonly
            if (minDate != null) this.minDate = minDate
            if (maxDate != null) this.maxDate = maxDate
            if (daysOfWeekDisabled.isNotEmpty()) this.daysOfWeekDisabled = daysOfWeekDisabled
            if (enabledDates.isNotEmpty()) this.enabledDates = enabledDates
            if (disabledDates.isNotEmpty()) this.disabledDates = disabledDates
            this.inline = inline
            this.keepOpen = keepOpen
            this.focusOnShow = focusOnShow
            this.locale = language
            this.icons = obj {
                this.time = "far fa-clock"
                this.date = "far fa-calendar"
                this.up = "fas fa-arrow-up"
                this.down = "fas fa-arrow-down"
                this.previous = "fas fa-chevron-left"
                this.next = "fas fa-chevron-right"
                this.today = "fas fa-calendar-check"
                this.clear = "far fa-trash-alt"
                this.close = "far fa-times-circle"
            }
            this.tooltips = obj {
                this.today = ""
                this.clear = ""
                this.close = ""
                this.selectMonth = ""
                this.prevMonth = ""
                this.nextMonth = ""
                this.selectYear = ""
                this.prevYear = ""
                this.nextYear = ""
                this.selectDecade = ""
                this.prevDecade = ""
                this.nextDecade = ""
                this.prevCentury = ""
                this.nextCentury = ""
                this.pickHour = ""
                this.incrementHour = ""
                this.decrementHour = ""
                this.pickMinute = ""
                this.incrementMinute = ""
                this.decrementMinute = ""
                this.pickSecond = ""
                this.incrementSecond = ""
                this.decrementSecond = ""
                this.togglePeriod = ""
                this.selectTime = ""
            }
            this.keyBinds = obj {
                enter = {
                    self.togglePopup()
                }
            }
        })
    }

    private fun initEventHandlers() {
        this.getElementJQuery()?.on("dp.change") { e, _ ->
            val moment = e.asDynamic().date
            @Suppress("UnsafeCastFromDynamic")
            if (moment) {
                this.value = moment.toDate()
            } else {
                this.value = null
            }
            @Suppress("UnsafeCastFromDynamic")
            this.dispatchEvent("change", obj { detail = e })
        }
        this.getElementJQuery()?.on("dp.error") { e, _ ->
            this.value = null
            @Suppress("UnsafeCastFromDynamic")
            this.dispatchEvent("change", obj { detail = e })
        }
        this.getElementJQuery()?.on("dp.show") { _, _ ->
            val inTabulator = this.getElementJQuery()?.closest(".tabulator-cell")?.length == 1
            if (inTabulator) {
                val datepicker = jQuery("body").find(".bootstrap-datetimepicker-widget:last")
                val position = datepicker.offset()
                val parent = datepicker.parent()
                val parentPos = parent.offset()
                val width = datepicker.width()
                val parentWid = parent.width()
                datepicker.appendTo("body")
                @Suppress("UnsafeCastFromDynamic")
                datepicker.css(obj {
                    this.position = "absolute"
                    this.top = position.top
                    this.bottom = "auto"
                    this.left = position.left
                    this.right = "auto"
                })
                if (parentPos.left.toInt() + parentWid.toInt() < position.left.toInt() + width.toInt()) {
                    var newLeft = parentPos.left.toInt()
                    newLeft += parentWid.toInt() / 2
                    newLeft -= width.toInt() / 2
                    @Suppress("UnsafeCastFromDynamic")
                    datepicker.css(obj { this.left = newLeft })
                }
            }
        }
    }

    override fun bindAllJQueryListeners() {
        bindAllJQueryListeners(this, jqueryListenersMap)
    }

    override fun removeAllJQueryListeners() {
        removeAllJQueryListeners(this, jqueryListenersMap)
    }

    /**
     * Get value of date/time input control as String
     * @return value as a String
     */
    fun getValueAsString(): String? {
        return value?.toStringF(format)
    }

    /**
     * Makes the input element focused.
     */
    override fun focus() {
        input.focus()
    }

    /**
     * Makes the input element blur.
     */
    override fun blur() {
        input.blur()
    }


    override fun getState(): Date? = value

    override fun subscribe(observer: (Date?) -> Unit): () -> Unit {
        observers += observer
        observer(value)
        return {
            observers -= observer
        }
    }

    override fun setState(state: Date?) {
        value = state
    }

    companion object {
        init {
            BootstrapDatetimeModule.initialize()
        }
    }
}

/**
 * DSL builder extension function.
 *
 * It takes the same parameters as the constructor of the built component.
 */
fun Container.dateTimeInput(
    value: Date? = null, format: String = "YYYY-MM-DD HH:mm",
    className: String? = null,
    init: (DateTimeInput.() -> Unit)? = null
): DateTimeInput {
    val dateTimeInput = DateTimeInput(value, format, className, init)
    this.add(dateTimeInput)
    return dateTimeInput
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy