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

ysny.karibu-tools.karibu-tools.0.20.source-code.ComponentUtils.kt Maven / Gradle / Ivy

There is a newer version: 0.21
Show newest version
package com.github.mvysny.kaributools

import com.vaadin.flow.component.*
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.checkbox.Checkbox
import com.vaadin.flow.component.combobox.ComboBox
import com.vaadin.flow.component.datepicker.DatePicker
import com.vaadin.flow.component.formlayout.FormLayout
import com.vaadin.flow.component.html.Input
import com.vaadin.flow.component.tabs.Tab
import com.vaadin.flow.component.textfield.AbstractNumberField
import com.vaadin.flow.component.textfield.BigDecimalField
import com.vaadin.flow.component.textfield.PasswordField
import com.vaadin.flow.component.textfield.TextArea
import com.vaadin.flow.component.textfield.TextField
import com.vaadin.flow.dom.DomEventListener
import com.vaadin.flow.dom.DomListenerRegistration
import com.vaadin.flow.router.Location
import java.lang.reflect.Method
import kotlin.streams.toList

/**
 * Fires given event on the component.
 */
public fun Component.fireEvent(event: ComponentEvent<*>) {
    ComponentUtil.fireEvent(this, event)
}

/**
 * Adds [com.vaadin.flow.component.button.Button.click] functionality to all [ClickNotifier]s. This function directly calls
 * all click listeners, thus it avoids the roundtrip to client and back. It even works with browserless testing.
 * @param fromClient see [ComponentEvent.isFromClient], defaults to true.
 * @param button see [ClickEvent.getButton], defaults to 0.
 * @param clickCount see [ClickEvent.getClickCount], defaults to 1.
 */
public fun > T.serverClick(
    fromClient: Boolean = true,
    button: Int = 0,
    clickCount: Int = 1,
    shiftKey: Boolean = false,
    ctrlKey: Boolean = false,
    altKey: Boolean = false,
    metaKey: Boolean = false
) {
    (this as Component).fireEvent(ClickEvent(this,
        fromClient, -1, -1, -1, -1, clickCount, button, ctrlKey, shiftKey, altKey, metaKey))
}

/**
 * Sets the alignment of the text in the component. One of `center`, `left`, `right`, `justify`.
 */
public var Component.textAlign: String?
    get() = element.style.get("textAlign")
    set(value) { element.style.set("textAlign", value) }

/**
 * Sets or removes the `title` attribute on component's element.
 */
public var Component.tooltip: String?
    get() = element.getAttribute("title")
    set(value) { element.setOrRemoveAttribute("title", value) }

/**
 * Adds the right-click (context-menu) [listener] to the component. Also causes the right-click browser
 * menu not to be shown on this component (see [preventDefault]).
 */
public fun Component.addContextMenuListener(listener: DomEventListener): DomListenerRegistration =
        element.addEventListener("contextmenu", listener)
                .preventDefault()

/**
 * Makes the client-side listener call [Event.preventDefault()](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
 * on the event.
 *
 * @return this
 */
public fun DomListenerRegistration.preventDefault(): DomListenerRegistration = addEventData("event.preventDefault()")

/**
 * Removes the component from its parent. Does nothing if the component is not attached to a parent.
 */
public fun Component.removeFromParent() {
    (parent.orElse(null) as? HasComponents)?.remove(this)
}

/**
 * Finds component's parent, parent's parent (etc) which satisfies given [predicate].
 * Returns null if there is no such parent.
 */
public fun Component.findAncestor(predicate: (Component) -> Boolean): Component? =
        findAncestorOrSelf { it != this && predicate(it) }

/**
 * Finds component, component's parent, parent's parent (etc) which satisfies given [predicate].
 * Returns null if no component on the ancestor-or-self axis satisfies.
 */
public tailrec fun Component.findAncestorOrSelf(predicate: (Component) -> Boolean): Component? {
    if (predicate(this)) {
        return this
    }
    val p: Component = parent.orElse(null) ?: return null
    return p.findAncestorOrSelf(predicate)
}

/**
 * Checks if this component is nested in [potentialAncestor].
 */
public fun Component.isNestedIn(potentialAncestor: Component): Boolean =
        findAncestor { it == potentialAncestor } != null

/**
 * Checks whether this component is currently attached to a [UI].
 *
 * Returns true for attached components even if the UI itself is closed.
 */
@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // Added in Vaadin 14.7.0; let's keep it here for older Vaadins.
public fun Component.isAttached(): Boolean {
    // see https://github.com/vaadin/flow/issues/7911
    return element.node.isAttached
}

/**
 * Inserts this component as a child, right before an [existing] one.
 *
 * In case the specified component has already been added to another parent,
 * it will be removed from there and added to this one.
 */
public fun HasOrderedComponents<*>.insertBefore(newComponent: Component, existing: Component) {
    val parent: Component = requireNotNull(existing.parent.orElse(null)) { "$existing has no parent" }
    require(parent == this) { "$existing is not nested in $this" }
    addComponentAtIndex(indexOf(existing), newComponent)
}

/**
 * Return the location of the currently shown view. The function will report the current (old)
 * view in [com.vaadin.flow.router.BeforeLeaveEvent] and [com.vaadin.flow.router.BeforeEnterEvent].
 */
public val UI.currentViewLocation: Location get() = internals.activeViewLocation

/**
 * True when the component has any children. Alias for [hasChildren].
 *
 * Deprecated: poorly named. A `form.isNotEmpty` may be ambiguous - it may refer to
 * whether the form has any children, or whether the form value is not empty, or something else.
 */
@Deprecated("use hasChildren", replaceWith = ReplaceWith("hasChildren"))
public val HasComponents.isNotEmpty: Boolean get() = hasChildren

/**
 * True when the component has any children.
 */
public val HasComponents.hasChildren: Boolean get() = (this as Component).children.findFirst().isPresent

/**
 * True when the component has no children.
 *
 * Deprecated: poorly named. A `form.isEmpty` may be ambiguous - it may refer to
 * whether the form has any children, or whether the form value is empty, or something else.
 */
@Deprecated("use !hasChildren")
public val HasComponents.isEmpty: Boolean get() = !hasChildren

/**
 * Splits [classNames] by whitespaces to obtain individual class names, then
 * calls [HasStyle.addClassName] on each class name. Does nothing if the string
 * is blank.
 */
public fun HasStyle.addClassNames2(classNames: String) {
    // workaround for https://github.com/vaadin/flow/issues/11709
    classNames.splitByWhitespaces().forEach { addClassName(it) }
}

/**
 * Splits [classNames] by whitespaces to obtain individual class names, then
 * calls [addClassNames2] on each class name. Does nothing if the string
 * is blank.
 */
public fun HasStyle.addClassNames2(vararg classNames: String) {
    // workaround for https://github.com/vaadin/flow/issues/11709
    classNames.forEach { addClassNames2(it) }
}

/**
 * Splits [classNames] by whitespaces to obtain individual class names, then
 * calls [HasStyle.removeClassName] on each class name. Does nothing if the string
 * is blank.
 */
public fun HasStyle.removeClassNames2(classNames: String) {
    // workaround for https://github.com/vaadin/flow/issues/11709
    classNames.splitByWhitespaces().forEach { removeClassName(it) }
}

/**
 * Splits [classNames] by whitespaces to obtain individual class names, then
 * calls [removeClassNames2] on each class name. Does nothing if the string
 * is blank.
 */
public fun HasStyle.removeClassNames2(vararg classNames: String) {
    // workaround for https://github.com/vaadin/flow/issues/11709
    classNames.forEach { removeClassNames2(it) }
}

/**
 * Splits [classNames] by whitespaces to obtain individual class names, then
 * clears the class names and calls [addClassNames2] on each class name. Does nothing if the string
 * is blank.
 */
public fun HasStyle.setClassNames2(classNames: String) {
    // workaround for https://github.com/vaadin/flow/issues/11709
    style.clear()
    addClassNames2(classNames)
}

/**
 * Splits [classNames] by whitespaces to obtain individual class names, then
 * clears the class names and calls [addClassNames2] on each class name. Does nothing if the string
 * is blank.
 */
public fun HasStyle.setClassNames2(vararg classNames: String) {
    // workaround for https://github.com/vaadin/flow/issues/11709
    style.clear()
    addClassNames2(*classNames)
}

/**
 * In Vaadin 24.3.0.alpha6 the `HasPlaceholder` interface was introduced.
 */
private val _HasPlaceholder: Class<*>? =
    if (VaadinVersion.flow >= SemanticVersion(24, 3, 0, "alpha6")) {
        Class.forName("com.vaadin.flow.component.HasPlaceholder")
    } else {
        null
    }
private val methodGetPlaceholder: Method? =
    _HasPlaceholder?.getDeclaredMethod("getPlaceholder")
private val methodSetPlaceholder: Method? =
    _HasPlaceholder?.getDeclaredMethod("setPlaceholder", String::class.java)

/**
 * A component placeholder, usually shown when there's no value selected.
 * Not all components support a placeholder; those that don't return null.
 */
public var Component.placeholder: String?
    get() = when {
        _HasPlaceholder != null && _HasPlaceholder.isInstance(this) -> methodGetPlaceholder!!.invoke(this) as String?
        this is TextField -> placeholder
        this is TextArea -> placeholder
        this is PasswordField -> placeholder
        this is ComboBox<*> -> this.placeholder  // https://youtrack.jetbrains.com/issue/KT-24275
        this is DatePicker -> placeholder
        this is Input -> placeholder.orElse(null)
        this is BigDecimalField -> placeholder
        this is AbstractNumberField<*, *> -> placeholder
        else -> null
    }
    set(value) {
        when {
            _HasPlaceholder != null && _HasPlaceholder.isInstance(this) -> methodSetPlaceholder!!.invoke(this, value)
            this is TextField -> placeholder = value
            this is TextArea -> placeholder = value
            this is PasswordField -> placeholder = value
            this is ComboBox<*> -> this.placeholder = value
            this is DatePicker -> placeholder = value
            this is Input -> setPlaceholder(value)
            this is BigDecimalField -> placeholder = value
            this is AbstractNumberField<*, *> -> placeholder = value
            else -> throw IllegalStateException("${javaClass.simpleName} doesn't support setting placeholder")
        }
    }

/**
 * Concatenates texts from all elements placed in the `label` slot. This effectively
 * returns whatever was provided in the String label via [FormLayout.addFormItem].
 */
public val FormLayout.FormItem.label: String get() {
    val captions: List = children.toList().filter { it.element.getAttribute("slot") == "label" }
    return captions.joinToString("") { (it as? HasText)?.text ?: "" }
}

/**
 * The `HasLabel` interface has been introduced in Vaadin 21 but is missing in Vaadin 14.7 and lower.
 * Use reflection.
 */
private val _HasLabel: Class<*>? = try {
    Class.forName("com.vaadin.flow.component.HasLabel")
} catch (ex: ClassNotFoundException) {
    null
}
private val _HasLabel_getLabel: Method? = _HasLabel?.getDeclaredMethod("getLabel")
private val _HasLabel_setLabel: Method? = _HasLabel?.getDeclaredMethod("setLabel", String::class.java)

/**
 * Determines the component's `label` (usually it's the HTML element's `label` property, but it's [Checkbox.getLabel] for checkbox).
 * Intended to be used for fields such as [TextField].

 * *For `FormItem`:* Concatenates texts from all elements placed in the `label` slot. This effectively
 * returns whatever was provided in the String label via [FormLayout.addFormItem].
 *
 * Button is special: it has no label; the caption can be retrieved via [Button.getText].
 *
 * **WARNING:** the label is displayed by the component itself, rather than by the parent layout.
 * If a component doesn't contain necessary machinery
 * to display a label (for example doesn't respond to the JavaScript property 'label'), setting this property will have no visual effect.
 * For example, setting a label to a [FormLayout]
 * will show nothing since [FormLayout] doesn't display a label itself.
 * See [LabelWrapper] for a list of possible solutions.
 *
 * This unifying property may not be a good idea after all: See the discussion at https://github.com/vaadin/flow-components/issues/5129
 */
public var Component.label: String
    get() = when {
        _HasLabel != null && _HasLabel.isInstance(this) -> _HasLabel_getLabel!!.invoke(this) as String? ?: ""
        this is Checkbox -> label ?: ""
        this is FormLayout.FormItem -> this.label
        this is Tab -> this.label
        else -> element.getProperty("label") ?: ""
    }
    set(value) {
        when {
            _HasLabel != null && _HasLabel.isInstance(this) -> _HasLabel_setLabel!!.invoke(this, value)
            this is Checkbox -> label = value
            this is FormLayout.FormItem -> throw IllegalArgumentException("Setting the caption of FormItem is currently unsupported")
            this is Tab -> this.label = value
            else -> element.setProperty("label", value.ifBlank { null })
        }
    }

/**
 * The Component's caption: [Button.getText] for [Button], [label] for fields such as [TextField].
 *
 * Caption is generally displayed directly on the component (e.g. the Button text),
 * while [label] is displayed next to the component in a layout (e.g. a [TextField] nested in a form layout).
 *
 * **Deprecated:** this property was intended to unify captions and labels, but only managed to
 * create confusion between the two concepts. Also, there's only a [Button] which
 * has the notion of a caption. Will be removed with no replacement.
 */
@Suppress("DEPRECATION")
@Deprecated("don't use")
public var Component.caption: String
    get() = when (this) {
        is Button -> caption
        else -> label
    }
    set(value) {
        when (this) {
            is Button -> caption = value
            else -> label = value
        }
    }

/**
 * The Button's caption. Alias for [Button.getText].
 *
 * Caption is generally displayed directly on the component (e.g. the Button text),
 * while [label] is displayed next to the component in a layout (e.g. a [TextField] nested in a form layout).
 *
 * **Deprecated:** this property was intended to unify captions and labels, but only managed to
 * create confusion between the two concepts. Also, there's only a [Button] which
 * has the notion of a caption. Will be removed with no replacement.
 */
@Deprecated("don't use")
public var Button.caption: String
    get() = text
    set(value) {
        text = value
    }

internal fun HasElement.getChildComponentInSlot(slotName: String): Component? =
    element.getChildrenInSlot(slotName).firstOrNull()?.component?.get()

internal fun HasElement.setChildComponentToSlot(slotName: String, component: Component?) {
    element.clearSlot(slotName)
    if (component != null) {
        component.element.setAttribute("slot", slotName)
        element.appendChild(component.element)
    }
}

private val __Component_hasListener: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
    val m = Component::class.java.getDeclaredMethod("hasListener", Class::class.java)
    m.isAccessible = true
    m
}

/**
 * Checks whether the component has a listener registered for this event type. Calls [ComponentEventBus.hasListener].
 */
internal fun Component.hasListener(eventType: Class>): Boolean =
    __Component_hasListener.invoke(this, eventType) as Boolean

/**
 * Sets the `aria-label` attribute.
 */
public var Component.ariaLabel: String?
    get() = element.getAttribute("aria-label")
    set(value) {
        element.setOrRemoveAttribute("aria-label", value)
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy