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

com.vaadin.flow.component.timepicker.TimePicker Maven / Gradle / Ivy

There is a newer version: 24.4.12
Show newest version
/*
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.vaadin.flow.component.timepicker;

import java.time.Duration;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Stream;

import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.AbstractSinglePropertyField;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.Focusable;
import com.vaadin.flow.component.HasAriaLabel;
import com.vaadin.flow.component.HasPlaceholder;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.ClientValidationUtil;
import com.vaadin.flow.component.shared.HasAllowedCharPattern;
import com.vaadin.flow.component.shared.HasAutoOpen;
import com.vaadin.flow.component.shared.HasClearButton;
import com.vaadin.flow.component.shared.HasClientValidation;
import com.vaadin.flow.component.shared.HasOverlayClassName;
import com.vaadin.flow.component.shared.HasPrefix;
import com.vaadin.flow.component.shared.HasThemeVariant;
import com.vaadin.flow.component.shared.HasValidationProperties;
import com.vaadin.flow.component.shared.InputField;
import com.vaadin.flow.component.shared.ValidationUtil;
import com.vaadin.flow.data.binder.HasValidator;
import com.vaadin.flow.data.binder.ValidationResult;
import com.vaadin.flow.data.binder.ValidationStatusChangeEvent;
import com.vaadin.flow.data.binder.ValidationStatusChangeListener;
import com.vaadin.flow.data.binder.Validator;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.function.SerializableFunction;
import com.vaadin.flow.internal.StateTree;
import com.vaadin.flow.shared.Registration;

/**
 * Time Picker is an input field for entering or selecting a specific time. The
 * time can be entered directly using a keyboard or by choosing a value from a
 * set of predefined options presented in an overlay. The overlay opens when the
 * field is clicked or any input is entered when the field is focused.
 *
 * @author Vaadin Ltd
 */
@Tag("vaadin-time-picker")
@NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.4.4")
@JsModule("@vaadin/polymer-legacy-adapter/style-modules.js")
@NpmPackage(value = "@vaadin/time-picker", version = "24.4.4")
@JsModule("@vaadin/time-picker/src/vaadin-time-picker.js")
@JsModule("./vaadin-time-picker/timepickerConnector.js")
public class TimePicker
        extends AbstractSinglePropertyField
        implements Focusable, HasAllowedCharPattern, HasAriaLabel,
        HasAutoOpen, HasClearButton, HasClientValidation,
        InputField, LocalTime>,
        HasPrefix, HasOverlayClassName, HasThemeVariant,
        HasValidationProperties, HasValidator, HasPlaceholder {

    private static final SerializableFunction PARSER = valueFromClient -> {
        return valueFromClient == null || valueFromClient.isEmpty() ? null
                : LocalTime.parse(valueFromClient);
    };

    private static final SerializableFunction FORMATTER = valueFromModel -> {
        return valueFromModel == null ? "" : valueFromModel.toString();
    };

    private Locale locale;

    private LocalTime max;
    private LocalTime min;
    private boolean required;
    private StateTree.ExecutionRegistration pendingLocaleUpdate;

    private boolean manualValidationEnabled = false;

    private final CopyOnWriteArrayList> validationStatusChangeListeners = new CopyOnWriteArrayList<>();

    /**
     * Default constructor.
     */
    public TimePicker() {
        this((LocalTime) null, true);
    }

    /**
     * Convenience constructor to create a time picker with a pre-selected time.
     *
     * @param time
     *            the pre-selected time in the picker
     */
    public TimePicker(LocalTime time) {
        this(time, false);
    }

    /**
     * Convenience constructor to create a time picker with a pre-selected time.
     *
     * @param time
     *            the pre-selected time in the picker
     * @param isInitialValueOptional
     *            If {@code isInitialValueOptional} is {@code true} then the
     *            initial value is used only if element has no {@code "value"}
     *            property value, otherwise element {@code "value"} property is
     *            ignored and the initial value is set
     */
    private TimePicker(LocalTime time, boolean isInitialValueOptional) {
        super("value", time, String.class, PARSER, FORMATTER);

        // Initialize property value unless it has already been set from a
        // template
        if ((getElement().getProperty("value") == null
                || !isInitialValueOptional)) {
            setPresentationValue(time);
        }

        // workaround for https://github.com/vaadin/flow/issues/3496
        setInvalid(false);

        addValueChangeListener(e -> validate());

        getElement().addEventListener("unparsable-change", event -> {
            validate();
            fireValidationStatusChangeEvent();
        });

        getElement().addPropertyChangeListener("invalid", event -> fireEvent(
                new InvalidChangeEvent(this, event.isUserOriginated())));
    }

    /**
     * Convenience constructor to create a time picker with a label.
     *
     * @param label
     *            the label describing the time picker
     * @see #setLabel(String)
     */
    public TimePicker(String label) {
        this();
        setLabel(label);
    }

    /**
     * Convenience constructor to create a time picker with a pre-selected time
     * and a label.
     *
     * @param label
     *            the label describing the time picker
     * @param time
     *            the pre-selected time in the picker
     */
    public TimePicker(String label, LocalTime time) {
        this(time);
        setLabel(label);
    }

    /**
     * Convenience constructor to create a time picker with a
     * {@link ValueChangeListener}.
     *
     * @param listener
     *            the listener to receive value change events
     * @see #addValueChangeListener(HasValue.ValueChangeListener)
     */
    public TimePicker(
            ValueChangeListener> listener) {
        this();
        addValueChangeListener(listener);
    }

    /**
     * Convenience constructor to create a time picker with a pre-selected time
     * and {@link ValueChangeListener}.
     *
     * @param time
     *            the pre-selected time in the picker
     * @param listener
     *            the listener to receive value change events
     * @see #addValueChangeListener(HasValue.ValueChangeListener)
     */
    public TimePicker(LocalTime time,
            ValueChangeListener> listener) {
        this(time);
        addValueChangeListener(listener);
    }

    /**
     * Convenience constructor to create a time picker with a label, a
     * pre-selected time and a {@link ValueChangeListener}.
     *
     * @param label
     *            the label describing the time picker
     * @param time
     *            the pre-selected time in the picker
     * @param listener
     *            the listener to receive value change events
     * @see #setLabel(String)
     * @see #addValueChangeListener(HasValue.ValueChangeListener)
     */
    public TimePicker(String label, LocalTime time,
            ValueChangeListener> listener) {
        this(time);
        setLabel(label);
        addValueChangeListener(listener);
    }

    /**
     * Sets the label for the time picker.
     *
     * @param label
     *            value for the {@code label} property in the time picker
     */
    public void setLabel(String label) {
        getElement().setProperty("label", label == null ? "" : label);
    }

    /**
     * Sets the selected time value of the component. The value can be cleared
     * by setting null.
     *
     * 

* The value will be truncated to millisecond precision, as that is the * maximum that the time picker supports. This means that * {@link #getValue()} might return a different value than what was passed * in. * * @param value * the LocalTime instance representing the selected time, or null */ @Override public void setValue(LocalTime value) { // Truncate the value to millisecond precision, as the is the maximum // that the time picker web component supports. if (value != null) { value = value.truncatedTo(ChronoUnit.MILLIS); } LocalTime oldValue = getValue(); boolean isOldValueEmpty = valueEquals(oldValue, getEmptyValue()); boolean isNewValueEmpty = valueEquals(value, getEmptyValue()); boolean isValueRemainedEmpty = isOldValueEmpty && isNewValueEmpty; boolean isInputValuePresent = isInputValuePresent(); // When the value is cleared programmatically, reset hasInputValue // so that the following validation doesn't treat this as bad input. if (isNewValueEmpty) { getElement().setProperty("_hasInputValue", false); } super.setValue(value); // Clear the input element from possible bad input. if (isValueRemainedEmpty && isInputValuePresent) { // The check for value presence guarantees that a non-empty value // won't get cleared when setValue(null) and setValue(...) are // subsequently called within one round-trip. // Flow only sends the final component value to the client // when you update the value multiple times during a round-trip // and the final value is sent in place of the first one, so // `executeJs` can end up invoked after a non-empty value is set. getElement() .executeJs("if (!this.value) this._inputElementValue = ''"); validate(); fireValidationStatusChangeEvent(); } } /** * Gets the label of the time picker. * * @return the {@code label} property of the time picker */ public String getLabel() { return getElement().getProperty("label"); } @Override public void setAriaLabel(String ariaLabel) { getElement().setProperty("accessibleName", ariaLabel); } @Override public Optional getAriaLabel() { return Optional.ofNullable(getElement().getProperty("accessibleName")); } @Override public void setAriaLabelledBy(String labelledBy) { getElement().setProperty("accessibleNameRef", labelledBy); } @Override public Optional getAriaLabelledBy() { return Optional .ofNullable(getElement().getProperty("accessibleNameRef")); } @Override public Validator getDefaultValidator() { return (value, context) -> checkValidity(value); } @Override public Registration addValidationStatusChangeListener( ValidationStatusChangeListener listener) { return Registration.addAndRemove(validationStatusChangeListeners, listener); } /** * Notifies Binder that it needs to revalidate the component since the * component's validity state may have changed. Note, there is no need to * notify Binder separately in the case of a ValueChangeEvent, as Binder * already listens to this event and revalidates automatically. */ private void fireValidationStatusChangeEvent() { ValidationStatusChangeEvent event = new ValidationStatusChangeEvent<>( this, !isInvalid()); validationStatusChangeListeners .forEach(listener -> listener.validationStatusChanged(event)); } private ValidationResult checkValidity(LocalTime value) { boolean hasNonParsableValue = Objects.equals(value, getEmptyValue()) && isInputValuePresent(); if (hasNonParsableValue) { return ValidationResult.error(""); } ValidationResult greaterThanMaxValidation = ValidationUtil .checkGreaterThanMax(value, max); if (greaterThanMaxValidation.isError()) { return greaterThanMaxValidation; } ValidationResult smallThanMinValidation = ValidationUtil .checkSmallerThanMin(value, min); if (smallThanMinValidation.isError()) { return smallThanMinValidation; } return ValidationResult.ok(); } /** * Performs a server-side validation of the given value. This is needed * because it is possible to circumvent the client side validation * constraints using browser development tools. */ private boolean isInvalid(LocalTime value) { var requiredValidation = ValidationUtil.checkRequired(required, value, getEmptyValue()); return requiredValidation.isError() || checkValidity(value).isError(); } /** * Returns whether the input element has a value or not. * * @return true if the input element's value is populated, * false otherwise */ @Synchronize(property = "_hasInputValue", value = "has-input-value-changed") protected boolean isInputValuePresent() { return getElement().getProperty("_hasInputValue", false); } /** * Sets whether the time picker is marked as input required. * * @param required * the boolean value to set */ public void setRequired(boolean required) { getElement().setProperty("required", required); this.required = required; } @Override public void setRequiredIndicatorVisible(boolean requiredIndicatorVisible) { super.setRequiredIndicatorVisible(requiredIndicatorVisible); this.required = requiredIndicatorVisible; } /** * Determines whether the time picker is marked as input required. *

* This property is not synchronized automatically from the client side, so * the returned value may not be the same as in client side. * * @return {@code true} if the input is required, {@code false} otherwise */ public boolean isRequired() { return getElement().getProperty("required", false); } /** * Sets the {@code step} property of the time picker using duration. It * specifies the intervals for the displayed items in the time picker * dropdown and also the displayed time format. *

* The set step needs to evenly divide a day or an hour and has to be larger * than 0 milliseconds. By default, the format is {@code hh:mm} (same as * * {@code Duration.ofHours(1)} *

* If the step is less than 60 seconds, the format will be changed to * {@code hh:mm:ss} and it can be in {@code hh:mm:ss.fff} format, when the * step is less than 1 second. *

* NOTE: If the step is less than 900 seconds, the dropdown is * hidden. *

* NOTE: changing the step to a larger duration can cause a new * {@link com.vaadin.flow.component.HasValue.ValueChangeEvent} to be fired * if some parts (eg. seconds) is discarded from the value. * * @param step * the step to set, not {@code null} and should divide a day or * an hour evenly */ public void setStep(Duration step) { Objects.requireNonNull(step, "Step cannot be null"); getElement().setProperty("step", StepsUtil.convertDurationToStepsValue(step)); } /** * Gets the step of the time picker. * *

* This property is not synchronized automatically from the client side, so * the returned value may not be the same as in client side. * * @return the {@code step} property from the picker, unit seconds */ public Duration getStep() { // if step was not set by the user, then assume default value of the // time picker web component if (!getElement().hasProperty("step")) { return StepsUtil.DEFAULT_WEB_COMPONENT_STEP; } double step = getElement().getProperty("step", 0.0); return StepsUtil.convertStepsValueToDuration(step); } /** * {@code invalid-changed} event is sent when the invalid state changes. */ public static class InvalidChangeEvent extends ComponentEvent { private final boolean invalid; public InvalidChangeEvent(TimePicker source, boolean fromClient) { super(source, fromClient); this.invalid = source.isInvalid(); } public boolean isInvalid() { return invalid; } } /** * Adds a listener for {@code invalid-changed} events fired by the * webcomponent. * * @param listener * the listener * @return a {@link Registration} for removing the event listener */ public Registration addInvalidChangeListener( ComponentEventListener listener) { return addListener(InvalidChangeEvent.class, listener); } @Override public void setManualValidation(boolean enabled) { this.manualValidationEnabled = enabled; } /** * Performs server-side validation of the current value. This is needed * because it is possible to circumvent the client-side validation * constraints using browser development tools. */ protected void validate() { if (!this.manualValidationEnabled) { setInvalid(isInvalid(getValue())); } } @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); initConnector(); requestLocaleUpdate(); ClientValidationUtil.preventWebComponentFromModifyingInvalidState(this); } private void initConnector() { // can't run this with getElement().executeJs(...) since then // setLocale might be called before this causing client side error runBeforeClientResponse(ui -> ui.getPage().executeJs( "window.Vaadin.Flow.timepickerConnector.initLazy($0)", getElement())); } /** * Set the Locale for the Time Picker. The displayed time will be formatted * by the browser using the given locale. *

* By default, the locale is {@code null} until the component is attached to * an UI, and then locale is set to {@link UI#getLocale()}, unless a locale * has been explicitly set before that. *

* The time formatting is done in the browser using the Date.toLocaleTimeString() * function. *

* If for some reason the browser doesn't support the given locale, the * en-US locale is used. *

* NOTE: only the language + country/region codes are used. This * means that the script and variant information is not used and supported. * NOTE: timezone related data is not supported. NOTE: changing * the locale does not cause a new * {@link com.vaadin.flow.component.HasValue.ValueChangeEvent} to be * fired. * * @param locale * the locale set to the time picker, cannot be [@code null} */ public void setLocale(Locale locale) { Objects.requireNonNull(locale, "Locale must not be null."); if (locale.getLanguage().isEmpty()) { throw new UnsupportedOperationException("Given Locale " + locale.getDisplayName() + " is not supported by time picker because it is missing the language information."); } this.locale = locale; requestLocaleUpdate(); } /** * Gets the Locale for this time picker. *

* By default, the locale is {@code null} until the component is attached to * an UI, and then locale is set to {@link UI#getLocale()}, unless * {@link #setLocale(Locale)} has been explicitly called before that. * * @return the locale used for this time picker */ @Override public Locale getLocale() { if (locale != null) { return locale; } else { return super.getLocale(); } } private void requestLocaleUpdate() { getUI().ifPresent(ui -> { if (pendingLocaleUpdate != null) { pendingLocaleUpdate.remove(); } pendingLocaleUpdate = ui.beforeClientResponse(this, context -> { pendingLocaleUpdate = null; executeLocaleUpdate(); }); }); } private void executeLocaleUpdate() { Locale appliedLocale = getLocale(); // we could support script & variant, but that requires more work on // client side to detect the different // number characters for other scripts (current only Arabic there) StringBuilder bcp47LanguageTag = new StringBuilder( appliedLocale.getLanguage()); if (!appliedLocale.getCountry().isEmpty()) { bcp47LanguageTag.append("-").append(appliedLocale.getCountry()); } runBeforeClientResponse(ui -> getElement().callJsFunction( "$connector.setLocale", bcp47LanguageTag.toString())); } /** * Sets the minimum time in the time picker. Times before that will be * disabled in the popup. * * @param min * the minimum time that is allowed to be selected, or * null to remove any minimum constraints */ public void setMin(LocalTime min) { this.min = min; String minString = format(min); getElement().setProperty("min", minString == null ? "" : minString); } /** * Gets the minimum time in the time picker. Time before that will be * disabled in the popup. * * @return the minimum time that is allowed to be selected, or * null if there's no minimum */ public LocalTime getMin() { return this.min; } /** * Sets the maximum time in the time picker. Times after that will be * disabled in the popup. * * @param max * the maximum time that is allowed to be selected, or * null to remove any maximum constraints */ public void setMax(LocalTime max) { this.max = max; String maxString = format(max); getElement().setProperty("max", maxString == null ? "" : maxString); } /** * Gets the maximum time in the time picker. Times after that will be * disabled in the popup. * * @return the maximum time that is allowed to be selected, or * null if there's no maximum */ public LocalTime getMax() { return this.max; } private void runBeforeClientResponse(SerializableConsumer command) { getElement().getNode().runWhenAttached(ui -> ui .beforeClientResponse(this, context -> command.accept(ui))); } /** * Returns a stream of all the available locales that are supported by the * time picker component. *

* This is a shorthand for {@link Locale#getAvailableLocales()} where all * locales without the {@link Locale#getLanguage()} have been filtered out, * as the browser cannot localize the time for those. * * @return a stream of the available locales that are supported by the time * picker component * @see #setLocale(Locale) * @see Locale#getAvailableLocales() * @see Locale#getLanguage() */ public static Stream getSupportedAvailableLocales() { return Stream.of(Locale.getAvailableLocales()) .filter(locale -> !locale.getLanguage().isEmpty()); } private static String format(LocalTime time) { return time != null ? time.toString() : null; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy