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

time-pickerpackage.src.vaadin-time-picker.js Maven / Gradle / Ivy

The newest version!
/**
 * @license
 * Copyright (c) 2018 - 2024 Vaadin Ltd.
 * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
 */
import '@vaadin/input-container/src/vaadin-input-container.js';
import './vaadin-time-picker-combo-box.js';
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
import { InputController } from '@vaadin/field-base/src/input-controller.js';
import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
import { PatternMixin } from '@vaadin/field-base/src/pattern-mixin.js';
import { inputFieldShared } from '@vaadin/field-base/src/styles/input-field-shared-styles.js';
import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';

const MIN_ALLOWED_TIME = '00:00:00.000';
const MAX_ALLOWED_TIME = '23:59:59.999';

registerStyles('vaadin-time-picker', inputFieldShared, { moduleId: 'vaadin-time-picker-styles' });

/**
 * `` is a Web Component providing a time-selection field.
 *
 * ```html
 * 
 * ```
 * ```js
 * timePicker.value = '14:30';
 * ```
 *
 * When the selected `value` is changed, a `value-changed` event is triggered.
 *
 * ### Styling
 *
 * The following custom properties are available for styling:
 *
 * Custom property                          | Description                | Default
 * -----------------------------------------|----------------------------|---------
 * `--vaadin-field-default-width`           | Default width of the field | `12em`
 * `--vaadin-time-picker-overlay-width`     | Width of the overlay       | `auto`
 * `--vaadin-time-picker-overlay-max-height`| Max height of the overlay  | `65vh`
 *
 * `` provides the same set of shadow DOM parts and state attributes as ``.
 * See [``](#/elements/vaadin-text-field) for the styling documentation.
 *
 * In addition to `` parts, the following parts are available for theming:
 *
 * Part name       | Description
 * ----------------|----------------
 * `toggle-button` | The toggle button
 *
 * In addition to `` state attributes, the following state attributes are available for theming:
 *
 * Attribute | Description
 * ----------|------------------------------------------
 * `opened`  | Set when the time-picker dropdown is open
 *
 * ### Internal components
 *
 * In addition to `` itself, the following internal
 * components are themable:
 *
 * - `` - has the same API as [``](#/elements/vaadin-combo-box-light).
 * - `` - has the same API as [``](#/elements/vaadin-overlay).
 * - `` - has the same API as [``](#/elements/vaadin-item).
 * - [``](#/elements/vaadin-input-container) - an internal element wrapping the input.
 *
 * Note: the `theme` attribute value set on `` is
 * propagated to the internal components listed above.
 *
 * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
 *
 * ### Change events
 *
 * Depending on the nature of the value change that the user attempts to commit e.g. by pressing Enter,
 * the component can fire either a `change` event or an `unparsable-change` event:
 *
 * Value change             | Event
 * :------------------------|:------------------
 * empty => parsable        | change
 * empty => unparsable      | unparsable-change
 * parsable => empty        | change
 * parsable => parsable     | change
 * parsable => unparsable   | change
 * unparsable => empty      | unparsable-change
 * unparsable => parsable   | change
 * unparsable => unparsable | unparsable-change
 *
 * @fires {Event} change - Fired when the user commits a value change.
 * @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
 * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
 * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
 * @fires {CustomEvent} validated - Fired whenever the field is validated.
 *
 * @customElement
 * @extends HTMLElement
 * @mixes ElementMixin
 * @mixes ThemableMixin
 * @mixes InputControlMixin
 * @mixes PatternMixin
 */
class TimePicker extends PatternMixin(InputControlMixin(ThemableMixin(ElementMixin(PolymerElement)))) {
  static get is() {
    return 'vaadin-time-picker';
  }

  static get template() {
    return html`
      

      
`; } static get properties() { return { /** * The time value for this element. * * Supported time formats are in ISO 8601: * - `hh:mm` (default) * - `hh:mm:ss` * - `hh:mm:ss.fff` * @type {string} */ value: { type: String, notify: true, value: '', }, /** * True if the dropdown is open, false otherwise. */ opened: { type: Boolean, notify: true, value: false, reflectToAttribute: true, }, /** * Minimum time allowed. * * Supported time formats are in ISO 8601: * - `hh:mm` * - `hh:mm:ss` * - `hh:mm:ss.fff` * @type {string} */ min: { type: String, value: '', }, /** * Maximum time allowed. * * Supported time formats are in ISO 8601: * - `hh:mm` * - `hh:mm:ss` * - `hh:mm:ss.fff` * @type {string} */ max: { type: String, value: '', }, /** * Defines the time interval (in seconds) between the items displayed * in the time selection box. The default is 1 hour (i.e. `3600`). * * It also configures the precision of the value string. By default * the component formats values as `hh:mm` but setting a step value * lower than one minute or one second, format resolution changes to * `hh:mm:ss` and `hh:mm:ss.fff` respectively. * * Unit must be set in seconds, and for correctly configuring intervals * in the dropdown, it need to evenly divide a day. * * Note: it is possible to define step that is dividing an hour in inexact * fragments (i.e. 5760 seconds which equals 1 hour 36 minutes), but it is * not recommended to use it for better UX experience. */ step: { type: Number, }, /** * Set true to prevent the overlay from opening automatically. * @attr {boolean} auto-open-disabled */ autoOpenDisabled: Boolean, /** * A space-delimited list of CSS class names to set on the overlay element. * * @attr {string} overlay-class */ overlayClass: { type: String, }, /** @private */ __dropdownItems: { type: Array, }, /** * The object used to localize this component. * To change the default localization, replace the entire * _i18n_ object or just the property you want to modify. * * The object has the following JSON structure: * * ``` * { * // A function to format given `Object` as * // time string. Object is in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }` * formatTime: (time) => { * // returns a string representation of the given * // object in `hh` / 'hh:mm' / 'hh:mm:ss' / 'hh:mm:ss.fff' - formats * }, * * // A function to parse the given text to an `Object` in the format * // `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`. * // Must properly parse (at least) text * // formatted by `formatTime`. * parseTime: text => { * // Parses a string in object/string that can be formatted by`formatTime`. * } * } * ``` * * Both `formatTime` and `parseTime` need to be implemented * to ensure the component works properly. * * @type {!TimePickerI18n} */ i18n: { type: Object, value: () => { return { formatTime: (time) => { if (!time) { return; } const pad = (num = 0, fmt = '00') => (fmt + num).substr((fmt + num).length - fmt.length); // Always display hour and minute let timeString = `${pad(time.hours)}:${pad(time.minutes)}`; // Adding second and millisecond depends on resolution if (time.seconds !== undefined) { timeString += `:${pad(time.seconds)}`; } if (time.milliseconds !== undefined) { timeString += `.${pad(time.milliseconds, '000')}`; } return timeString; }, parseTime: (text) => { // Parsing with RegExp to ensure correct format const MATCH_HOURS = '(\\d|[0-1]\\d|2[0-3])'; const MATCH_MINUTES = '(\\d|[0-5]\\d)'; const MATCH_SECONDS = MATCH_MINUTES; const MATCH_MILLISECONDS = '(\\d{1,3})'; const re = new RegExp( `^${MATCH_HOURS}(?::${MATCH_MINUTES}(?::${MATCH_SECONDS}(?:\\.${MATCH_MILLISECONDS})?)?)?$`, 'u', ); const parts = re.exec(text); if (parts) { // Allows setting the milliseconds with hundreds and tens precision if (parts[4]) { while (parts[4].length < 3) { parts[4] += '0'; } } return { hours: parts[1], minutes: parts[2], seconds: parts[3], milliseconds: parts[4] }; } }, }; }, }, /** @private */ _comboBoxValue: { type: String, observer: '__comboBoxValueChanged', }, /** @private */ _inputContainer: Object, }; } static get observers() { return [ '__updateAriaAttributes(__dropdownItems, opened, inputElement)', '__updateDropdownItems(i18n.*, min, max, step)', ]; } static get constraints() { return [...super.constraints, 'min', 'max']; } /** * Used by `InputControlMixin` as a reference to the clear button element. * @protected * @return {!HTMLElement} */ get clearElement() { return this.$.clearButton; } /** * The input element's value when it cannot be parsed as a time, and an empty string otherwise. * * @private * @return {string} */ get __unparsableValue() { if (this._inputElementValue && !this.i18n.parseTime(this._inputElementValue)) { return this._inputElementValue; } return ''; } /** @protected */ ready() { super.ready(); this.addController( new InputController( this, (input) => { this._setInputElement(input); this._setFocusElement(input); this.stateTarget = input; this.ariaTarget = input; }, { // The "search" word is a trick to prevent Safari from enabling AutoFill, // which is causing click issues: // https://github.com/vaadin/web-components/issues/6817#issuecomment-2268229567 uniqueIdPrefix: 'search-input', }, ), ); this.addController(new LabelledInputController(this.inputElement, this._labelController)); this._inputContainer = this.shadowRoot.querySelector('[part~="input-field"]'); this._tooltipController = new TooltipController(this); this._tooltipController.setShouldShow((timePicker) => !timePicker.opened); this._tooltipController.setPosition('top'); this._tooltipController.setAriaTarget(this.inputElement); this.addController(this._tooltipController); } /** * Override method inherited from `InputMixin` to forward the input to combo-box. * @protected * @override */ _inputElementChanged(input) { super._inputElementChanged(input); if (input) { this.$.comboBox._setInputElement(input); } } /** * Opens the dropdown list. */ open() { if (!this.disabled && !this.readonly) { this.opened = true; } } /** * Closes the dropdown list. */ close() { this.opened = false; } /** * Returns true if the current input value satisfies all constraints (if any). * You can override this method for custom validations. * * @return {boolean} True if the value is valid */ checkValidity() { return !!( this.inputElement.checkValidity() && (!this.value || this._timeAllowed(this.i18n.parseTime(this.value))) && (!this._comboBoxValue || this.i18n.parseTime(this._comboBoxValue)) ); } /** * @param {boolean} focused * @override * @protected */ _setFocused(focused) { super._setFocused(focused); if (!focused) { // Do not validate when focusout is caused by document // losing focus, which happens on browser tab switch. if (document.hasFocus()) { this.validate(); } } } /** @private */ __validDayDivisor(step) { // Valid if undefined, or exact divides a day, or has millisecond resolution return !step || (24 * 3600) % step === 0 || (step < 1 && ((step % 1) * 1000) % 1 === 0); } /** * Override an event listener from `KeyboardMixin`. * @param {!KeyboardEvent} e * @protected */ _onKeyDown(e) { super._onKeyDown(e); if (this.readonly || this.disabled || this.__dropdownItems.length) { return; } const stepResolution = (this.__validDayDivisor(this.step) && this.step) || 60; if (e.keyCode === 40) { this.__onArrowPressWithStep(-stepResolution); } else if (e.keyCode === 38) { this.__onArrowPressWithStep(stepResolution); } } /** * Override an event listener from `KeyboardMixin`. * Do not call `super` in order to override clear * button logic defined in `InputControlMixin`. * @param {Event} event * @protected */ _onEscape() { // Do nothing, the internal combo-box handles Escape. } /** @private */ __onArrowPressWithStep(step) { const objWithStep = this.__addStep(this.__getMsec(this.__memoValue), step, true); this.__memoValue = objWithStep; // Setting `_comboBoxValue` property triggers the synchronous // observer where the value can be parsed again, so we set // this flag to ensure it does not alter the value. this.__useMemo = true; this._comboBoxValue = this.i18n.formatTime(objWithStep); this.__useMemo = false; this.__commitValueChange(); } /** * Depending on the nature of the value change that has occurred since * the last commit attempt, triggers validation and fires an event: * * Value change | Event * -------------------------|------------------- * empty => parsable | change * empty => unparsable | unparsable-change * parsable => empty | change * parsable => parsable | change * parsable => unparsable | change * unparsable => empty | unparsable-change * unparsable => parsable | change * unparsable => unparsable | unparsable-change * * @private */ __commitValueChange() { const unparsableValue = this.__unparsableValue; if (this.__committedValue !== this.value) { this.validate(); this.dispatchEvent(new CustomEvent('change', { bubbles: true })); } else if (this.__committedUnparsableValue !== unparsableValue) { this.validate(); this.dispatchEvent(new CustomEvent('unparsable-change')); } this.__committedValue = this.value; this.__committedUnparsableValue = unparsableValue; } /** * Returning milliseconds from Object in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }` * @private */ __getMsec(obj) { let result = ((obj && obj.hours) || 0) * 60 * 60 * 1000; result += ((obj && obj.minutes) || 0) * 60 * 1000; result += ((obj && obj.seconds) || 0) * 1000; result += (obj && parseInt(obj.milliseconds)) || 0; return result; } /** * Returning seconds from Object in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }` * @private */ __getSec(obj) { let result = ((obj && obj.hours) || 0) * 60 * 60; result += ((obj && obj.minutes) || 0) * 60; result += (obj && obj.seconds) || 0; result += (obj && obj.milliseconds / 1000) || 0; return result; } /** * Returning Object in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }` * from the result of adding step value in milliseconds to the milliseconds amount. * With `precision` parameter rounding the value to the closest step valid interval. * @private */ __addStep(msec, step, precision) { // If the time is `00:00` and step changes value downwards, it should be considered as `24:00` if (msec === 0 && step < 0) { msec = 24 * 60 * 60 * 1000; } const stepMsec = step * 1000; const diffToNext = msec % stepMsec; if (stepMsec < 0 && diffToNext && precision) { msec -= diffToNext; } else if (stepMsec > 0 && diffToNext && precision) { msec -= diffToNext - stepMsec; } else { msec += stepMsec; } const hh = Math.floor(msec / 1000 / 60 / 60); msec -= hh * 1000 * 60 * 60; const mm = Math.floor(msec / 1000 / 60); msec -= mm * 1000 * 60; const ss = Math.floor(msec / 1000); msec -= ss * 1000; return { hours: hh < 24 ? hh : 0, minutes: mm, seconds: ss, milliseconds: msec }; } /** @private */ __updateDropdownItems(_i18n, min, max, step) { const minTimeObj = this.__validateTime(this.__parseISO(min || MIN_ALLOWED_TIME)); const minSec = this.__getSec(minTimeObj); const maxTimeObj = this.__validateTime(this.__parseISO(max || MAX_ALLOWED_TIME)); const maxSec = this.__getSec(maxTimeObj); this.__dropdownItems = this.__generateDropdownList(minSec, maxSec, step); if (step !== this.__oldStep) { this.__oldStep = step; const parsedObj = this.__validateTime(this.__parseISO(this.value)); this.__updateValue(parsedObj); } if (this.value) { this._comboBoxValue = this.i18n.formatTime(this.i18n.parseTime(this.value)); } } /** @private */ __updateAriaAttributes(items, opened, input) { if (items === undefined || input === undefined) { return; } if (items.length === 0) { input.removeAttribute('role'); input.removeAttribute('aria-expanded'); } else { input.setAttribute('role', 'combobox'); input.setAttribute('aria-expanded', !!opened); } } /** @private */ __generateDropdownList(minSec, maxSec, step) { if (step < 15 * 60 || !this.__validDayDivisor(step)) { return []; } const generatedList = []; // Default step in overlay items is 1 hour if (!step) { step = 3600; } let time = -step + minSec; while (time + step >= minSec && time + step <= maxSec) { const timeObj = this.__validateTime(this.__addStep(time * 1000, step)); time += step; const formatted = this.i18n.formatTime(timeObj); generatedList.push({ label: formatted, value: formatted }); } return generatedList; } /** * Override an observer from `InputMixin`. * @protected * @override */ _valueChanged(value, oldValue) { const parsedObj = (this.__memoValue = this.__parseISO(value)); const newValue = this.__formatISO(parsedObj) || ''; // Mark value set programmatically by the user // as committed for the change event detection. if (!this.__keepCommittedValue) { this.__committedValue = value; this.__committedUnparsableValue = ''; } if (value !== '' && value !== null && !parsedObj) { // Value can not be parsed, reset to the old one. this.value = oldValue === undefined ? '' : oldValue; } else if (value !== newValue) { // Value can be parsed (e.g. 12 -> 12:00), adjust. this.value = newValue; } else if (this.__keepInvalidInput) { // User input could not be parsed and was reset // to empty string, do not update input value. delete this.__keepInvalidInput; } else { this.__updateInputValue(parsedObj); } this._toggleHasValue(this._hasValue); } /** @private */ __comboBoxValueChanged(value, oldValue) { if (value === '' && oldValue === undefined) { return; } const parsedObj = this.__useMemo ? this.__memoValue : this.i18n.parseTime(value); const newValue = this.i18n.formatTime(parsedObj) || ''; if (parsedObj) { if (value !== newValue) { this._comboBoxValue = newValue; } else { this.__keepCommittedValue = true; this.__updateValue(parsedObj); this.__keepCommittedValue = false; } } else { // If the user input can not be parsed, set a flag // that prevents `__valueChanged` from removing the input // after setting the value property to an empty string below. if (this.value !== '' && value !== '') { this.__keepInvalidInput = true; } this.__keepCommittedValue = true; this.value = ''; this.__keepCommittedValue = false; } } /** @private */ __onComboBoxChange(event) { event.stopPropagation(); this.__commitValueChange(); } /** * Synchronizes the `_hasInputValue` property with the internal combo-box's one. * * @private */ __onComboBoxHasInputValueChanged() { this._hasInputValue = this.$.comboBox._hasInputValue; } /** @private */ __updateValue(obj) { const timeString = this.__formatISO(this.__validateTime(obj)) || ''; this.value = timeString; } /** @private */ __updateInputValue(obj) { const timeString = this.i18n.formatTime(this.__validateTime(obj)) || ''; this._comboBoxValue = timeString; } /** @private */ __validateTime(timeObject) { if (timeObject) { const stepSegment = this.__getStepSegment(); timeObject.hours = parseInt(timeObject.hours); timeObject.minutes = parseInt(timeObject.minutes || 0); timeObject.seconds = stepSegment < 3 ? undefined : parseInt(timeObject.seconds || 0); timeObject.milliseconds = stepSegment < 4 ? undefined : parseInt(timeObject.milliseconds || 0); } return timeObject; } /** @private */ __getStepSegment() { if (this.step % 3600 === 0) { // Accept hours return 1; } else if (this.step % 60 === 0 || !this.step) { // Accept minutes return 2; } else if (this.step % 1 === 0) { // Accept seconds return 3; } else if (this.step < 1) { // Accept milliseconds return 4; } return undefined; } /** @private */ __formatISO(time) { // The default i18n formatter implementation is ISO 8601 compliant return TimePicker.properties.i18n.value().formatTime(time); } /** @private */ __parseISO(text) { // The default i18n parser implementation is ISO 8601 compliant return TimePicker.properties.i18n.value().parseTime(text); } /** * Returns true if `time` satisfies the `min` and `max` constraints (if any). * * @param {!TimePickerTime} time Value to check against constraints * @return {boolean} True if `time` satisfies the constraints * @protected */ _timeAllowed(time) { const parsedMin = this.i18n.parseTime(this.min || MIN_ALLOWED_TIME); const parsedMax = this.i18n.parseTime(this.max || MAX_ALLOWED_TIME); return ( (!this.__getMsec(parsedMin) || this.__getMsec(time) >= this.__getMsec(parsedMin)) && (!this.__getMsec(parsedMax) || this.__getMsec(time) <= this.__getMsec(parsedMax)) ); } /** * Override method inherited from `InputControlMixin`. * @protected */ _onClearButtonClick() {} /** * Override method inherited from `InputConstraintsMixin`. * @protected */ _onChange() {} /** * Override method inherited from `InputMixin`. * @protected */ _onInput() {} /** * Fired when the user commits a value change. * * @event change */ } defineCustomElement(TimePicker); export { TimePicker };




© 2015 - 2024 Weber Informatics LLC | Privacy Policy