date-time-pickerpackage.src.vaadin-date-time-picker.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-webcomponents Show documentation
Show all versions of vaadin-webcomponents Show documentation
Mvnpm composite: Vaadin webcomponents
The newest version!
/**
* @license
* Copyright (c) 2019 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
import { DatePicker } from '@vaadin/date-picker/src/vaadin-date-picker.js';
import {
dateEquals,
formatUTCISODate,
normalizeUTCDate,
parseUTCDate,
} from '@vaadin/date-picker/src/vaadin-date-picker-helper.js';
import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';
import { inputFieldShared } from '@vaadin/field-base/src/styles/input-field-shared-styles.js';
import { TimePicker } from '@vaadin/time-picker/src/vaadin-time-picker.js';
import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
registerStyles('vaadin-date-time-picker', inputFieldShared, { moduleId: 'vaadin-date-time-picker' });
/**
* @typedef {object} TimePickerTime
* @property {string | number} hours
* @property {string | number} minutes
* @property {string | number} [seconds]
* @property {string | number} [milliseconds]
*/
// Find a property definition from the prototype chain of a Polymer element class
function getPropertyFromPrototype(clazz, prop) {
while (clazz) {
if (clazz.properties && clazz.properties[prop]) {
return clazz.properties[prop];
}
clazz = Object.getPrototypeOf(clazz);
}
}
const datePickerI18nDefaults = getPropertyFromPrototype(DatePicker, 'i18n').value();
const timePickerI18nDefaults = getPropertyFromPrototype(TimePicker, 'i18n').value();
const datePickerI18nProps = Object.keys(datePickerI18nDefaults);
const timePickerI18nProps = Object.keys(timePickerI18nDefaults);
/**
* A controller to initialize slotted picker.
*
* @private
*/
class PickerSlotController extends SlotController {
constructor(host, type) {
super(host, `${type}-picker`, `vaadin-${type}-picker`, {
initializer: (picker, host) => {
const prop = `__${type}Picker`;
host[prop] = picker;
},
});
}
}
/**
* `` is a Web Component providing a date time selection field.
*
* ```html
*
* ```
*
* ```js
* dateTimePicker.value = '2019-09-16T15:00';
* ```
*
* When the selected `value` is changed, a `value-changed` event is triggered.
*
* ### Styling
*
* The following shadow DOM parts are available for styling:
*
* Part name | Description
* ---------------------|----------------
* `label` | The slotted label element wrapper
* `helper-text` | The slotted helper text element wrapper
* `error-message` | The slotted error message element wrapper
* `required-indicator` | The `required` state indicator element
*
* The following state attributes are available for styling:
*
* Attribute | Description | Part name
* --------------------|-------------------------------------------|------------
* `disabled` | Set when the element is disabled | :host
* `focused` | Set when the element is focused | :host
* `focus-ring` | Set when the element is keyboard focused | :host
* `readonly` | Set when the element is readonly | :host
* `invalid` | Set when the element is invalid | :host
* `has-label` | Set when the element has a label | :host
* `has-value` | Set when the element has a value | :host
* `has-helper` | Set when the element has helper text | :host
* `has-error-message` | Set when the element has an error message | :host
*
* ### Internal components
*
* The following components are created by `` and placed in light DOM:
*
* - [``](#/elements/vaadin-date-picker).
* - [``](#/elements/vaadin-time-picker).
*
* Note: the `theme` attribute value set on `` is
* propagated to these components.
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*
* @fires {Event} change - Fired when the user commits a value change.
* @fires {CustomEvent} invalid-changed - Fired when the `invalid` 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 FocusMixin
* @mixes DisabledMixin
* @mixes FieldMixin
*/
class DateTimePicker extends FieldMixin(DisabledMixin(FocusMixin(ThemableMixin(ElementMixin(PolymerElement))))) {
static get template() {
return html`
`;
}
static get is() {
return 'vaadin-date-time-picker';
}
static get properties() {
return {
/**
* The name of the control, which is submitted with the form data.
*/
name: {
type: String,
},
/**
* The value for this element.
*
* Supported date time format is based on ISO 8601 (without a time zone designator):
* - Minute precision `"YYYY-MM-DDThh:mm"` (default)
* - Second precision `"YYYY-MM-DDThh:mm:ss"`
* - Millisecond precision `"YYYY-MM-DDThh:mm:ss.fff"`
* @type {string}
*/
value: {
type: String,
notify: true,
value: '',
observer: '__valueChanged',
},
/**
* The earliest allowed value (date and time) that can be selected. All earlier values will be disabled.
*
* Supported date time format is based on ISO 8601 (without a time zone designator):
* - Minute precision `"YYYY-MM-DDThh:mm"`
* - Second precision `"YYYY-MM-DDThh:mm:ss"`
* - Millisecond precision `"YYYY-MM-DDThh:mm:ss.fff"`
*
* @type {string | undefined}
*/
min: {
type: String,
observer: '__minChanged',
},
/**
* The latest value (date and time) that can be selected. All later values will be disabled.
*
* Supported date time format is based on ISO 8601 (without a time zone designator):
* - Minute precision `"YYYY-MM-DDThh:mm"`
* - Second precision `"YYYY-MM-DDThh:mm:ss"`
* - Millisecond precision `"YYYY-MM-DDThh:mm:ss.fff"`
*
* @type {string | undefined}
*/
max: {
type: String,
observer: '__maxChanged',
},
/**
* The earliest value that can be selected. All earlier values will be disabled.
* @private
*/
__minDateTime: {
type: Date,
value: '',
},
/**
* The latest value that can be selected. All later values will be disabled.
* @private
*/
__maxDateTime: {
type: Date,
value: '',
},
/**
* A placeholder string for the date field.
* @attr {string} date-placeholder
*/
datePlaceholder: {
type: String,
},
/**
* A placeholder string for the time field.
* @attr {string} time-placeholder
*/
timePlaceholder: {
type: String,
},
/**
* 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 time part of the value string. By default
* the component formats time 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.
*/
step: {
type: Number,
},
/**
* Date which should be visible in the date picker overlay when there is no value selected.
*
* The same date formats as for the `value` property are supported but without the time part.
* @attr {string} initial-position
*/
initialPosition: String,
/**
* Set true to display ISO-8601 week numbers in the calendar. Notice that
* displaying week numbers is only supported when `i18n.firstDayOfWeek`
* is 1 (Monday).
* @attr {boolean} show-week-numbers
*/
showWeekNumbers: {
type: Boolean,
value: false,
},
/**
* Set to true to prevent the overlays from opening automatically.
* @attr {boolean} auto-open-disabled
*/
autoOpenDisabled: Boolean,
/**
* Set to true to make this element read-only.
* @type {boolean}
*/
readonly: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
/**
* Specify that this control should have input focus when the page loads.
* @type {boolean}
*/
autofocus: {
type: Boolean,
},
/**
* The current selected date time.
* @private
*/
__selectedDateTime: {
type: Date,
},
/**
* The object used to localize this component.
* To change the default localization, replace the entire
* `i18n` object or just the properties you want to modify.
*
* The object is a combination of the i18n properties supported by
* [``](#/elements/vaadin-date-picker) and
* [``](#/elements/vaadin-time-picker).
* @type {!DateTimePickerI18n}
*/
i18n: {
type: Object,
value: () => ({ ...datePickerI18nDefaults, ...timePickerI18nDefaults }),
},
/**
* A space-delimited list of CSS class names to set on the overlay elements
* of the internal components controlled by the ``:
*
* - [``](#/elements/vaadin-date-picker#property-overlayClass)
* - [``](#/elements/vaadin-time-picker#property-overlayClass)
*
* @attr {string} overlay-class
*/
overlayClass: {
type: String,
},
/**
* The current slotted date picker.
* @private
*/
__datePicker: {
type: HTMLElement,
observer: '__datePickerChanged',
},
/**
* The current slotted time picker.
* @private
*/
__timePicker: {
type: HTMLElement,
observer: '__timePickerChanged',
},
};
}
static get observers() {
return [
'__selectedDateTimeChanged(__selectedDateTime)',
'__datePlaceholderChanged(datePlaceholder, __datePicker)',
'__timePlaceholderChanged(timePlaceholder, __timePicker)',
'__stepChanged(step, __timePicker)',
'__initialPositionChanged(initialPosition, __datePicker)',
'__showWeekNumbersChanged(showWeekNumbers, __datePicker)',
'__requiredChanged(required, __datePicker, __timePicker)',
'__invalidChanged(invalid, __datePicker, __timePicker)',
'__disabledChanged(disabled, __datePicker, __timePicker)',
'__readonlyChanged(readonly, __datePicker, __timePicker)',
'__i18nChanged(i18n, __datePicker, __timePicker)',
'__autoOpenDisabledChanged(autoOpenDisabled, __datePicker, __timePicker)',
'__themeChanged(_theme, __datePicker, __timePicker)',
'__overlayClassChanged(overlayClass, __datePicker, __timePicker)',
'__pickersChanged(__datePicker, __timePicker)',
'__labelOrAccessibleNameChanged(label, accessibleName, i18n, __datePicker, __timePicker)',
];
}
constructor() {
super();
// Default value for "min" and "max" properties of vaadin-date-picker (for removing constraint)
this.__defaultDateMinMaxValue = undefined;
// Default value for "min" property of vaadin-time-picker (for removing constraint)
this.__defaultTimeMinValue = '00:00:00.000';
// Default value for "max" property of vaadin-time-picker (for removing constraint)
this.__defaultTimeMaxValue = '23:59:59.999';
this.__changeEventHandler = this.__changeEventHandler.bind(this);
this.__valueChangedEventHandler = this.__valueChangedEventHandler.bind(this);
this.__openedChangedEventHandler = this.__openedChangedEventHandler.bind(this);
}
/** @private */
get __inputs() {
return [this.__datePicker, this.__timePicker];
}
/** @private */
get __formattedValue() {
const [dateValue, timeValue] = this.__inputs.map((picker) => picker.value);
if (dateValue && timeValue) {
return [dateValue, timeValue].join('T');
}
return '';
}
/** @protected */
ready() {
super.ready();
this._datePickerController = new PickerSlotController(this, 'date');
this.addController(this._datePickerController);
this._timePickerController = new PickerSlotController(this, 'time');
this.addController(this._timePickerController);
if (this.autofocus && !this.disabled) {
window.requestAnimationFrame(() => this.focus());
}
this.setAttribute('role', 'group');
this._tooltipController = new TooltipController(this);
this.addController(this._tooltipController);
this._tooltipController.setPosition('top');
this._tooltipController.setShouldShow((target) => {
return target.__datePicker && !target.__datePicker.opened && target.__timePicker && !target.__timePicker.opened;
});
this.ariaTarget = this;
}
focus() {
if (this.__datePicker) {
this.__datePicker.focus();
}
}
/**
* Override method inherited from `FocusMixin` to validate on blur.
* @param {boolean} focused
* @protected
* @override
*/
_setFocused(focused) {
super._setFocused(focused);
// Do not validate when focusout is caused by document
// losing focus, which happens on browser tab switch.
if (!focused && document.hasFocus()) {
this.validate();
}
}
/**
* Override method inherited from `FocusMixin` to not remove focused
* state when focus moves between pickers or to the overlay.
* @param {FocusEvent} event
* @return {boolean}
* @protected
* @override
*/
_shouldRemoveFocus(event) {
const target = event.relatedTarget;
const overlayContent = this.__datePicker._overlayContent;
if (
this.__datePicker.contains(target) ||
this.__timePicker.contains(target) ||
(overlayContent && overlayContent.contains(target))
) {
return false;
}
return true;
}
/** @private */
__syncI18n(target, source, props = Object.keys(source.i18n)) {
props.forEach((prop) => {
// eslint-disable-next-line no-prototype-builtins
if (source.i18n && source.i18n.hasOwnProperty(prop)) {
target.set(`i18n.${prop}`, source.i18n[prop]);
}
});
}
/** @private */
__changeEventHandler(event) {
event.stopPropagation();
if (this.__dispatchChangeForValue === this.value) {
this.validate();
this.__dispatchChange();
}
this.__dispatchChangeForValue = undefined;
}
/** @private */
__openedChangedEventHandler() {
const opened = this.__datePicker.opened || this.__timePicker.opened;
this.style.pointerEvents = opened ? 'auto' : '';
}
/** @private */
__addInputListeners(node) {
node.addEventListener('change', this.__changeEventHandler);
node.addEventListener('value-changed', this.__valueChangedEventHandler);
node.addEventListener('opened-changed', this.__openedChangedEventHandler);
}
/** @private */
__removeInputListeners(node) {
node.removeEventListener('change', this.__changeEventHandler);
node.removeEventListener('value-changed', this.__valueChangedEventHandler);
node.removeEventListener('opened-changed', this.__openedChangedEventHandler);
}
/** @private */
__isDefaultPicker(picker, type) {
const controller = this[`_${type}PickerController`];
return controller && picker === controller.defaultNode;
}
/** @private */
__datePickerChanged(newDatePicker, existingDatePicker) {
if (!newDatePicker) {
return;
}
if (existingDatePicker) {
// Remove an existing date picker
this.__removeInputListeners(existingDatePicker);
existingDatePicker.remove();
}
this.__addInputListeners(newDatePicker);
if (this.__isDefaultPicker(newDatePicker, 'date')) {
// Synchronize properties to default date picker
newDatePicker.placeholder = this.datePlaceholder;
newDatePicker.invalid = this.invalid;
newDatePicker.initialPosition = this.initialPosition;
newDatePicker.showWeekNumbers = this.showWeekNumbers;
this.__syncI18n(newDatePicker, this, datePickerI18nProps);
} else {
// Synchronize properties from slotted date picker
this.datePlaceholder = newDatePicker.placeholder;
this.initialPosition = newDatePicker.initialPosition;
this.showWeekNumbers = newDatePicker.showWeekNumbers;
this.__syncI18n(this, newDatePicker, datePickerI18nProps);
}
// Min and max are always synchronized from date time picker (host) to inner fields because time picker
// min and max need to be dynamically set depending on currently selected date instead of simple propagation
newDatePicker.min = this.__formatDateISO(this.__minDateTime, this.__defaultDateMinMaxValue);
newDatePicker.max = this.__formatDateISO(this.__maxDateTime, this.__defaultDateMinMaxValue);
// Disable default internal validation for the component
newDatePicker.validate = () => {};
newDatePicker._validateInput = () => {};
}
/** @private */
__timePickerChanged(newTimePicker, existingTimePicker) {
if (!newTimePicker) {
return;
}
if (existingTimePicker) {
// Remove an existing time picker
this.__removeInputListeners(existingTimePicker);
existingTimePicker.remove();
}
this.__addInputListeners(newTimePicker);
if (this.__isDefaultPicker(newTimePicker, 'time')) {
// Synchronize properties to default time picker
newTimePicker.placeholder = this.timePlaceholder;
newTimePicker.step = this.step;
newTimePicker.invalid = this.invalid;
this.__syncI18n(newTimePicker, this, timePickerI18nProps);
} else {
// Synchronize properties from slotted time picker
this.timePlaceholder = newTimePicker.placeholder;
this.step = newTimePicker.step;
this.__syncI18n(this, newTimePicker, timePickerI18nProps);
}
// Min and max are always synchronized from parent to slotted because time picker min and max
// need to be dynamically set depending on currently selected date instead of simple propagation
this.__updateTimePickerMinMax();
// Disable default internal validation for the component
newTimePicker.validate = () => {};
}
/** @private */
__updateTimePickerMinMax() {
if (this.__timePicker && this.__datePicker) {
const selectedDate = this.__parseDate(this.__datePicker.value);
const isMinMaxSameDay = dateEquals(this.__minDateTime, this.__maxDateTime, normalizeUTCDate);
const oldTimeValue = this.__timePicker.value;
if ((this.__minDateTime && dateEquals(selectedDate, this.__minDateTime, normalizeUTCDate)) || isMinMaxSameDay) {
this.__timePicker.min = this.__dateToIsoTimeString(this.__minDateTime);
} else {
this.__timePicker.min = this.__defaultTimeMinValue;
}
if ((this.__maxDateTime && dateEquals(selectedDate, this.__maxDateTime, normalizeUTCDate)) || isMinMaxSameDay) {
this.__timePicker.max = this.__dateToIsoTimeString(this.__maxDateTime);
} else {
this.__timePicker.max = this.__defaultTimeMaxValue;
}
// If time picker automatically adjusts the time value due to the new min or max
// revert the time value
if (this.__timePicker.value !== oldTimeValue) {
this.__timePicker.value = oldTimeValue;
}
}
}
/** @private */
__i18nChanged(i18n, datePicker, timePicker) {
if (datePicker) {
datePicker.i18n = { ...datePicker.i18n, ...i18n };
}
if (timePicker) {
timePicker.i18n = { ...timePicker.i18n, ...i18n };
}
}
/** @private */
__labelOrAccessibleNameChanged(label, accessibleName, i18n, datePicker, timePicker) {
const name = accessibleName || label || '';
if (datePicker) {
datePicker.accessibleName = `${name} ${i18n.dateLabel || ''}`.trim();
}
if (timePicker) {
timePicker.accessibleName = `${name} ${i18n.timeLabel || ''}`.trim();
}
}
/** @private */
__datePlaceholderChanged(datePlaceholder, datePicker) {
if (datePicker) {
datePicker.placeholder = datePlaceholder;
}
}
/** @private */
__timePlaceholderChanged(timePlaceholder, timePicker) {
if (timePicker) {
timePicker.placeholder = timePlaceholder;
}
}
/** @private */
__stepChanged(step, timePicker) {
if (timePicker && timePicker.step !== step) {
timePicker.step = step;
}
}
/** @private */
__initialPositionChanged(initialPosition, datePicker) {
if (datePicker) {
datePicker.initialPosition = initialPosition;
}
}
/** @private */
__showWeekNumbersChanged(showWeekNumbers, datePicker) {
if (datePicker) {
datePicker.showWeekNumbers = showWeekNumbers;
}
}
/** @private */
__invalidChanged(invalid, datePicker, timePicker) {
if (datePicker) {
datePicker.invalid = invalid;
}
if (timePicker) {
timePicker.invalid = invalid;
}
}
/** @private */
__requiredChanged(required, datePicker, timePicker) {
if (datePicker) {
datePicker.required = required;
}
if (timePicker) {
timePicker.required = required;
}
if (this.__oldRequired && !required) {
this.validate();
}
this.__oldRequired = required;
}
/** @private */
__disabledChanged(disabled, datePicker, timePicker) {
if (datePicker) {
datePicker.disabled = disabled;
}
if (timePicker) {
timePicker.disabled = disabled;
}
}
/** @private */
__readonlyChanged(readonly, datePicker, timePicker) {
if (datePicker) {
datePicker.readonly = readonly;
}
if (timePicker) {
timePicker.readonly = readonly;
}
}
/**
* String (ISO date) to Date object
* @param {string} str e.g. 'yyyy-mm-dd'
* @return {Date | undefined}
* @private
*/
__parseDate(str) {
return parseUTCDate(str);
}
/**
* Date object to string (ISO date)
* @param {Date} date
* @param {string} defaultValue
* @return {string} e.g. 'yyyy-mm-dd' (or defaultValue when date is falsy)
* @private
*/
__formatDateISO(date, defaultValue) {
if (!date) {
return defaultValue;
}
return formatUTCISODate(date);
}
/**
* Custom time object to string (ISO time)
* @param {!TimePickerTime} time Time components as properties { hours, minutes, seconds, milliseconds }
* @return {string} e.g. 'hh:mm', 'hh:mm:ss', 'hh:mm:ss.fff'
* @private
*/
__formatTimeISO(time) {
return timePickerI18nDefaults.formatTime(time);
}
/**
* String (ISO time) to custom time object
* @param {string} str e.g. 'hh:mm', 'hh:mm:ss', 'hh:mm:ss.fff'
* @return {!TimePickerTime | undefined} Time components as properties { hours, minutes, seconds, milliseconds }
* @private
*/
__parseTimeISO(str) {
return timePickerI18nDefaults.parseTime(str);
}
/**
* String (ISO date time) to Date object
* @param {string} str e.g. 'yyyy-mm-ddThh:mm', 'yyyy-mm-ddThh:mm:ss', 'yyyy-mm-ddThh:mm:ss.fff'
* @return {Date | undefined}
* @private
*/
__parseDateTime(str) {
const [dateValue, timeValue] = str.split('T');
/* c8 ignore next 3 */
if (!(dateValue && timeValue)) {
return;
}
/** @type {Date} */
const date = this.__parseDate(dateValue);
if (!date) {
return;
}
const time = this.__parseTimeISO(timeValue);
if (!time) {
return;
}
date.setUTCHours(parseInt(time.hours));
date.setUTCMinutes(parseInt(time.minutes || 0));
date.setUTCSeconds(parseInt(time.seconds || 0));
date.setUTCMilliseconds(parseInt(time.milliseconds || 0));
return date;
}
/**
* Date object to string (ISO date time)
* @param {Date} date
* @return {string} e.g. 'yyyy-mm-ddThh:mm', 'yyyy-mm-ddThh:mm:ss', 'yyyy-mm-ddThh:mm:ss.fff'
* (depending on precision defined by "step" property)
* @private
*/
__formatDateTime(date) {
if (!date) {
return '';
}
const dateValue = this.__formatDateISO(date, '');
const timeValue = this.__dateToIsoTimeString(date);
return `${dateValue}T${timeValue}`;
}
/**
* Date object to string (ISO time)
* @param {Date} date
* @return {string} e.g. 'hh:mm', 'hh:mm:ss', 'hh:mm:ss.fff' (depending on precision defined by "step" property)
* @private
*/
__dateToIsoTimeString(date) {
return this.__formatTimeISO(
this.__validateTime({
hours: date.getUTCHours(),
minutes: date.getUTCMinutes(),
seconds: date.getUTCSeconds(),
milliseconds: date.getUTCMilliseconds(),
}),
);
}
/**
* @param {!TimePickerTime} timeObject
* @return {!TimePickerTime}
* @private
*/
__validateTime(timeObject) {
if (timeObject) {
const stepSegment = this.__getStepSegment();
timeObject.seconds = stepSegment < 3 ? undefined : timeObject.seconds;
timeObject.milliseconds = stepSegment < 4 ? undefined : timeObject.milliseconds;
}
return timeObject;
}
/**
* Returns true if the current input value satisfies all constraints (if any)
*
* You can override the `checkValidity` method for custom validations.
* @return {boolean}
*/
checkValidity() {
const hasInvalidFields = this.__inputs.some((input) => !input.checkValidity());
const hasEmptyFields = this.required && this.__inputs.some((el) => !el.value);
if (hasInvalidFields || hasEmptyFields) {
return false;
}
return true;
}
// Copied from vaadin-time-picker
/** @private */
__getStepSegment() {
const step = this.step == null ? 60 : parseFloat(this.step);
if (step % 3600 === 0) {
// Accept hours
return 1;
} else if (step % 60 === 0 || !step) {
// Accept minutes
return 2;
} else if (step % 1 === 0) {
// Accept seconds
return 3;
} else if (step < 1) {
// Accept milliseconds
return 4;
}
}
/**
* @param {Date} date1
* @param {Date} date2
* @return {boolean}
* @private
*/
__dateTimeEquals(date1, date2) {
if (!dateEquals(date1, date2, normalizeUTCDate)) {
return false;
}
return (
date1.getUTCHours() === date2.getUTCHours() &&
date1.getUTCMinutes() === date2.getUTCMinutes() &&
date1.getUTCSeconds() === date2.getUTCSeconds() &&
date1.getUTCMilliseconds() === date2.getUTCMilliseconds()
);
}
/** @private */
__handleDateTimeChange(property, parsedProperty, value, oldValue) {
if (!value) {
this[property] = '';
this[parsedProperty] = '';
return;
}
const dateTime = this.__parseDateTime(value);
if (!dateTime) {
// Invalid date, revert to old value
this[property] = oldValue;
return;
}
if (!this.__dateTimeEquals(this[parsedProperty], dateTime)) {
this[parsedProperty] = dateTime;
}
}
/** @private */
__valueChanged(value, oldValue) {
this.__handleDateTimeChange('value', '__selectedDateTime', value, oldValue);
if (oldValue !== undefined) {
this.__dispatchChangeForValue = value;
}
this.toggleAttribute('has-value', !!value);
this.__updateTimePickerMinMax();
}
/** @private */
__dispatchChange() {
this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
}
/** @private */
__minChanged(value, oldValue) {
this.__handleDateTimeChange('min', '__minDateTime', value, oldValue);
if (this.__datePicker) {
this.__datePicker.min = this.__formatDateISO(this.__minDateTime, this.__defaultDateMinMaxValue);
}
this.__updateTimePickerMinMax();
if (this.__datePicker && this.__timePicker && this.value) {
this.validate();
}
}
/** @private */
__maxChanged(value, oldValue) {
this.__handleDateTimeChange('max', '__maxDateTime', value, oldValue);
if (this.__datePicker) {
this.__datePicker.max = this.__formatDateISO(this.__maxDateTime, this.__defaultDateMinMaxValue);
}
this.__updateTimePickerMinMax();
if (this.__datePicker && this.__timePicker && this.value) {
this.validate();
}
}
/** @private */
__selectedDateTimeChanged(selectedDateTime) {
const formattedValue = this.__formatDateTime(selectedDateTime);
if (this.value !== formattedValue) {
this.value = formattedValue;
}
// Setting the date/time picker value below triggers validation of the components.
// If the inputs are slotted (e.g. when using the Java API) and have an initial value this can
// happen before date picker ready() which would throw an error when date picker is trying to read
// `this.$.input` (as a result of value change triggered by setting the value).
// Workaround the problem by setting custom field value only if date picker is ready.
const isDatePickerReady = Boolean(this.__datePicker && this.__datePicker.$);
if (isDatePickerReady && !this.__ignoreInputValueChange) {
// Ignore value changes until both inputs have a value updated
// TODO: This logic clears both fields if one of them is cleared :(
this.__ignoreInputValueChange = true;
const [dateValue, timeValue] = this.value.split('T');
this.__datePicker.value = dateValue || '';
this.__timePicker.value = timeValue || '';
this.__ignoreInputValueChange = false;
}
}
/** @private */
__valueChangedEventHandler() {
if (this.__ignoreInputValueChange) {
return;
}
const value = this.__formattedValue;
const [date, time] = value.split('T');
this.__ignoreInputValueChange = true;
this.__updateTimePickerMinMax();
if (date && time) {
if (value !== this.value) {
this.value = value;
}
} else {
this.value = '';
}
this.__ignoreInputValueChange = false;
}
/** @private */
__autoOpenDisabledChanged(autoOpenDisabled, datePicker, timePicker) {
if (datePicker) {
datePicker.autoOpenDisabled = autoOpenDisabled;
}
if (timePicker) {
timePicker.autoOpenDisabled = autoOpenDisabled;
}
}
/** @private */
__themeChanged(theme, datePicker, timePicker) {
if (!datePicker || !timePicker) {
// Both pickers are not ready yet
return;
}
[datePicker, timePicker].forEach((picker) => {
if (theme) {
picker.setAttribute('theme', theme);
} else {
picker.removeAttribute('theme');
}
});
}
/** @private */
__overlayClassChanged(overlayClass, datePicker, timePicker) {
if (!datePicker || !timePicker) {
// Both pickers are not ready yet
return;
}
datePicker.overlayClass = overlayClass;
timePicker.overlayClass = overlayClass;
}
/** @private */
__pickersChanged(datePicker, timePicker) {
if (!datePicker || !timePicker) {
// Both pickers are not ready yet
return;
}
if (this.__isDefaultPicker(datePicker, 'date') !== this.__isDefaultPicker(timePicker, 'time')) {
// Both pickers are not replaced yet
return;
}
if (datePicker.value) {
// The new pickers have a value, update the component value
this.__valueChangedEventHandler();
} else if (this.value) {
// The component has a value, update the new pickers values
this.__selectedDateTimeChanged(this.__selectedDateTime);
if (this.min || this.max) {
this.validate();
}
}
}
/**
* Fired when the user commits a value change.
*
* @event change
*/
}
defineCustomElement(DateTimePicker);
export { DateTimePicker };