@progress/kendo-angular-dateinputs
Version:
Kendo UI for Angular Date Inputs Package - Everything you need to add date selection functionality to apps (DatePicker, TimePicker, DateInput, DateRangePicker, DateTimePicker, Calendar, and MultiViewCalendar).
1,396 lines • 107 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Component, HostBinding, ViewChild, ElementRef, Input, isDevMode, TemplateRef, ChangeDetectorRef, Output, EventEmitter, NgZone, ViewContainerRef, forwardRef, ContentChild, ChangeDetectionStrategy, Renderer2, Injector, } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, NgControl } from '@angular/forms';
import { Subscription, fromEvent } from 'rxjs';
import { tap } from 'rxjs/operators';
import { cloneDate, isEqual, getDate } from '@progress/kendo-date-math';
import { PopupService } from '@progress/kendo-angular-popup';
import { IntlService } from '@progress/kendo-angular-intl';
import { hasObservers, isControlRequired, KendoInput, Keys, MultiTabStop, ResizeSensorComponent, EventsOutsideAngularDirective } from '@progress/kendo-angular-common';
import { AdaptiveService } from '@progress/kendo-angular-utils';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { PickerService } from '../common/picker.service';
import { DisabledDatesService } from '../calendar/services/disabled-dates.service';
import { attributeNames, isPresent } from '../common/utils';
import { mergeDateAndTime, noop, lastMillisecondOfDate, isInRange, isValidRange, isWindowAvailable, getFillModeClass, getRoundedClass, getSizeClass, DEFAULT_FILL_MODE, DEFAULT_ROUNDED, DEFAULT_SIZE } from '../util';
import { CalendarComponent } from '../calendar/calendar.component';
import { TimeSelectorComponent } from '../timepicker/timeselector.component';
import { DateInputComponent } from '../dateinput/dateinput.component';
import { PreventableEvent } from '../preventable-event';
import { minValidator } from '../validators/min.validator';
import { maxValidator } from '../validators/max.validator';
import { disabledDatesValidator } from '../validators/disabled-date.validator';
import { TIME_PART } from '../timepicker/models/time-part.default';
import { MIN_DATE, MAX_DATE, MIN_TIME, MAX_TIME } from '../defaults';
import { CellTemplateDirective } from '../calendar/templates/cell-template.directive';
import { MonthCellTemplateDirective } from '../calendar/templates/month-cell-template.directive';
import { YearCellTemplateDirective } from '../calendar/templates/year-cell-template.directive';
import { DecadeCellTemplateDirective } from '../calendar/templates/decade-cell-template.directive';
import { CenturyCellTemplateDirective } from '../calendar/templates/century-cell-template.directive';
import { WeekNumberCellTemplateDirective } from '../calendar/templates/weeknumber-cell-template.directive';
import { HeaderTitleTemplateDirective } from '../calendar/templates/header-title-template.directive';
import { incompleteDateValidator } from '../validators/incomplete-date.validator';
import { calendarIcon, clockIcon, checkIcon } from '@progress/kendo-svg-icons';
import { ActionSheetComponent, ActionSheetTemplateDirective } from '@progress/kendo-angular-navigation';
import { HeaderTemplateDirective } from '../calendar/templates/header-template.directive';
import { FooterTemplateDirective } from '../calendar/templates/footer-template.directive';
import { TimeSelectorCustomMessagesComponent } from '../timepicker/localization/timeselector-custom-messages.component';
import { CalendarCustomMessagesComponent } from '../calendar/localization/calendar-custom-messages.component';
import { ButtonComponent } from '@progress/kendo-angular-buttons';
import { IconWrapperComponent } from '@progress/kendo-angular-icons';
import { NgIf, NgTemplateOutlet, NgClass } from '@angular/common';
import { DateInputCustomMessagesComponent } from '../dateinput/localization/dateinput-custom-messages.component';
import { LocalizedMessagesDirective } from './localization/localized-messages.directive';
import { touchEnabled } from '@progress/kendo-common';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-popup";
import * as i2 from "@progress/kendo-angular-intl";
import * as i3 from "../common/picker.service";
import * as i4 from "@progress/kendo-angular-l10n";
import * as i5 from "../calendar/services/disabled-dates.service";
import * as i6 from "@progress/kendo-angular-utils";
const timeFormatRegExp = new RegExp(`${TIME_PART.hour}|${TIME_PART.minute}|${TIME_PART.second}|${TIME_PART.dayperiod}|literal`);
const VALUE_DOC_LINK = 'https://www.telerik.com/kendo-angular-ui/components/dateinputs/datetimepicker/integration-with-json/';
const MIN_MAX_DOC_LINK = 'https://www.telerik.com/kendo-angular-ui/components/dateinputs/datetimepicker/date-time-limits/';
const DEFAULT_ACTIVE_TAB = 'date';
const DEFAULT_DATEINPUT_FORMAT = 'g';
const DEFAULT_TIMESELECTOR_FORMAT = 't';
const TWO_DIGIT_YEAR_MAX = 68;
const ACCEPT_BUTTON_SELECTOR = '.k-button.k-time-accept';
const CANCEL_BUTTON_SELECOTR = '.k-button.k-time-cancel';
const DATE_TAB_BUTTON_SELECTOR = '.k-button.k-group-start';
const TIME_TAB_BUTTON_SELECTOR = '.k-button.k-group-end';
const TODAY_BUTTON_SELECTOR = '.k-button.k-calendar-nav-today';
/**
* Represents the [Kendo UI DateTimePicker component for Angular]({% slug overview_datetimepicker %}).
*/
export class DateTimePickerComponent extends MultiTabStop {
popupService;
intl;
cdr;
pickerService;
ngZone;
wrapper;
localization;
disabledDatesService;
renderer;
injector;
adaptiveService;
/**
* @hidden
*/
calendarIcon = calendarIcon;
/**
* @hidden
*/
clockIcon = clockIcon;
/**
* @hidden
*/
hostClasses = true;
/**
* @hidden
*/
get disabledClass() {
return this.disabled;
}
/**
* @hidden
*/
toggleButton;
/**
* @hidden
*/
get dateInput() {
return this.pickerService.input;
}
/**
* @hidden
*/
get calendar() {
return this.pickerService.calendar;
}
/**
* @hidden
*/
get timeSelector() {
return this.pickerService.timeSelector;
}
/**
* @hidden
*/
focusableId;
/**
* Sets the format of the displayed Calendar week days' names.
* @default 'short'
*/
weekDaysFormat = "short";
/**
* Displays the days that fall out of the current month in the Calendar ([see example]({% slug datetimepicker_calendar_options %}#toc-displaying-other-month-days)).
* The default values per Calendar type are:
* - `infinite` - false
* - `classic` - true
*/
showOtherMonthDays;
/**
* Specifies the value of the DateTimePicker component.
*
* > The `value` has to be a valid [JavaScript `Date`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Date) instance or `null`.
*/
set value(value) {
this.verifyValue(value);
this._value = cloneDate(value);
this.setCalendarValue(value);
this.cdr.markForCheck();
}
get value() {
return this._value;
}
/**
* Specifies the date format for displaying the input value
* ([see example]({% slug formats_datetimepicker %}))
*
* Format value options:
* - `string` - Provide a `string` if a single format is going to be used regardless whether the input is focused or blurred.
* - [`FormatSettings`]({% slug api_dateinputs_formatsettings %}) - To display different formats when the component is focused or blurred, provide a settings object with specified `inputFormat` and `displayFormat` values.
*
* > If a [`FormatSettings`]({% slug api_dateinputs_formatsettings %}) object is provided, the `displayFormat` value will be used for the popup TimePicker.
*/
set format(format) {
this._format = format;
const displayFormat = this.getDisplayFormat(format);
this.timeSelectorFormat = this.getTimeSelectorFormat(displayFormat);
}
get format() {
return this._format;
}
/**
* The maximum year to assume to be from the current century when typing two-digit year value
* ([see example]({% slug formats_datetimepicker %}#toc-two-digit-year-format)).
*
* The default value is 68, indicating that typing any value less than 69
* will be assumed to be 20xx, while 69 and larger will be assumed to be 19xx.
*/
twoDigitYearMax = TWO_DIGIT_YEAR_MAX;
/**
* Specifies the [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the DateTimePicker.
*/
set tabindex(value) {
const tabindex = Number(value);
const defaultValue = 0;
this._tabindex = !isNaN(tabindex) ? tabindex : defaultValue;
}
get tabindex() {
return this.disabled ? -1 : this._tabindex;
}
/**
* Sets the dates of the DateTimePicker that will be disabled
* ([see example]({% slug disabled_dates_datetimepicker %})).
*/
set disabledDates(value) {
this._disabledDates = value;
this.disabledDatesService.initialize(value);
}
get disabledDates() {
return this._disabledDates;
}
/**
* Configures the popup settings of the DateTimePicker
* ([see example]({% slug datetimepicker_popup_options %}#toc-customizing-the-popup)).
*
* The available options are:
* - `animate: Boolean`—Controls the popup animation. By default, the open and close animations are enabled.
* - `appendTo: 'root' | 'component' | ViewContainerRef`—Controls the popup container. By default, the popup will be appended to the root component.
* - `popupClass: String`—Specifies a list of CSS classes that are used to style the popup.
*/
set popupSettings(settings) {
this._popupSettings = Object.assign({}, { animate: true }, settings);
}
get popupSettings() {
return this._popupSettings;
}
/**
* Sets the title of the input element of the DateTimePicker and the title text rendered
* in the header of the popup(action sheet). Applicable only when [`AdaptiveMode` is set to `auto`](slug:api_dateinputs_adaptivemode).
*/
adaptiveTitle = '';
/**
* Sets the subtitle text rendered in the header of the popup(action sheet).
* Applicable only when [`AdaptiveMode` is set to `auto`](slug:api_dateinputs_adaptivemode).
*/
adaptiveSubtitle = '';
/**
* Sets or gets the `disabled` property of the DateTimePicker and determines whether the component is active
* ([see example]({% slug disabled_datetimepicker %})).
* To learn how to disable the component in reactive forms, refer to the article on [Forms Support](slug:formssupport_datetimepicker#toc-managing-the-datetimepicker-disabled-state-in-reactive-forms).
*/
disabled = false;
/**
* Sets the read-only state of the DateTimePicker
* ([see example]({% slug readonly_datetimepicker %}#toc-read-only-datetimepicker)).
*
* @default false
*/
readonly = false;
/**
* Sets the read-only state of the DateTimePicker input field
* ([see example]({% slug readonly_datetimepicker %}#toc-read-only-input)).
*
* > Note that if you set the [`readonly`]({% slug api_dateinputs_datetimepickercomponent %}#toc-readonly) property value to `true`,
* the input will be rendered in a read-only state regardless of the `readOnlyInput` value.
*/
readOnlyInput = false;
/**
* Determines whether to display the **Cancel** button in the popup
* ([see example]({% slug datetimepicker_popup_options %}#toc-toggling-the-cancel-button)).
*/
cancelButton = true;
/**
* Defines the descriptions of the format sections in the input field
* ([see example]({% slug placeholders_datetimepicker %}#toc-format-sections-description)).
*/
formatPlaceholder;
/**
* Specifies the hint which is displayed by the DateTimePicker when its value is `null`
* ([see example]({% slug placeholders_datetimepicker %}#toc-text-hints)).
*/
placeholder;
/**
* Configures the incremental steps of the DateInput and the popup component of the TimePicker
* ([see example]({% slug incrementalsteps_datetimepicker %})).
*/
steps = {};
/**
* Specifies the focused date of the popup Calendar
* ([see example]({% slug datetimepicker_calendar_options %}#toc-focused-dates)).
*/
focusedDate;
/**
* Specifies the Calendar type.
*
* The possible values are:
* - `infinite` (default)
* - `classic`
*
*/
calendarType = 'infinite';
/**
* Determines whether to enable animation when navigating to previous/next Calendar view.
* Applies to the [`classic`]({% slug api_dateinputs_datetimepickercomponent %}#toc-calendartype) Calendar only.
*
* > This feature uses the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API). In order to run the animation in browsers that do not support it, you need the `web-animations-js` polyfill.
*
* @default false
*/
animateCalendarNavigation = false;
/**
* Determines whether to display a week number column in the `month` view of the popup Calendar
* ([see example]({% slug datetimepicker_calendar_options %}#toc-week-number-column)).
*/
weekNumber = false;
/**
* Specifies the smallest valid date.
* The Calendar will not display dates before this value.
* If the `min` value of the Calendar is selected, the TimePicker will not display
* time entries before the specified time portion of this value
* ([see example]({% slug dateranges_datetimepicker %})).
*/
set min(value) {
this._min = cloneDate(value);
this.calendarMin = getDate(value || MIN_DATE);
}
get min() {
return this._min;
}
/**
* Specifies the biggest valid date.
* The Calendar will not display dates after this value.
* If the `max` value of the Calendar is selected, the TimePicker will not display
* time entries after the specified time portion of this value
* ([see example]({% slug dateranges_datetimepicker %})).
*/
set max(value) {
this._max = cloneDate(value);
this.calendarMax = lastMillisecondOfDate(value || MAX_DATE);
}
get max() {
return this._max;
}
/**
* Determines whether the built-in min or max validators are enforced when validating a form
* ([see example](slug:dateranges_datetimepicker)).
*
* @default true
*/
rangeValidation = true;
/**
* Determines whether the built-in validator for disabled
* date ranges is enforced when validating a form
* ([see example](slug:disabled_dates_datetimepicker)).
*/
disabledDatesValidation = true;
/**
* Determines whether the built-in validation for incomplete dates is to be enforced when a form is being validated.
*/
incompleteDateValidation = false;
/**
* Determines whether to autocorrect invalid segments automatically.
*
* @default true
*/
autoCorrectParts = true;
/**
* Determines whether to automatically move to the next segment after the user completes the current one.
*
* @default true
*/
autoSwitchParts = true;
/**
* A string array representing custom keys, which will move the focus to the next date format segment.
*/
autoSwitchKeys = [];
/**
* Indicates whether the mouse scroll can be used to increase/decrease the time segments values.
*
* @default true
*/
enableMouseWheel = true;
/**
* Determines if the users should see a blinking caret inside the Date Input when possible.
*
* @default false
*/
allowCaretMode = false;
/**
* If set to `true`, renders a clear button after the input text or DateTimePicker value has been changed.
* Clicking this button resets the value of the component to `null` and triggers the `valueChange` event.
* @default false
*/
clearButton = false;
/**
* When enabled, the DateTimePicker will autofill the rest of the date to the current date when the component loses focus.
*
* @default false
*/
autoFill = false;
/**
* Enables or disables the adaptive mode. By default the adaptive rendering is disabled.
*/
adaptiveMode = 'none';
/**
* Sets the HTML attributes of the inner focusable input element. Attributes which are essential for certain component functionalities cannot be changed.
*/
inputAttributes;
/**
* Fires each time the user selects a new value
* ([see example](slug:events_datetimepicker)).
*/
valueChange = new EventEmitter();
/**
* Fires each time the popup is about to open
* ([see example](slug:events_datetimepicker)).
* This event is preventable. If you cancel the event by setting `event.preventDefault()`, the popup will remain closed.
*/
open = new EventEmitter();
/**
* Fires each time the popup is about to close
* ([see example](slug:events_datetimepicker)).
* This event is preventable. If you cancel the event by setting `event.preventDefault()`, the popup will remain open.
*/
close = new EventEmitter();
/**
* Fires each time the user focuses the component
* ([see example](slug:events_datetimepicker)).
*/
onFocus = new EventEmitter();
/**
* Fires each time the user blurs the component
* ([see example](slug:events_datetimepicker)).
*/
onBlur = new EventEmitter();
/**
* @hidden
*/
escape = new EventEmitter();
/**
* Indicates whether the component is currently open. That is when the popup or actionSheet is open.
*/
get isOpen() {
return this.actionSheet?.expanded || isPresent(this.popupRef);
}
/**
* Indicates whether the component or its popup content is focused.
*/
get isActive() {
return this._isActive;
}
set isActive(value) {
if (value) {
this.renderer.addClass(this.wrapper.nativeElement, 'k-focus');
}
else {
this.renderer.removeClass(this.wrapper.nativeElement, 'k-focus');
}
this._isActive = value;
}
/**
* Sets the active tab on opening the popup
* ([see example]({% slug datetimepicker_popup_options %}#toc-setting-the-default-tab)).
*/
set defaultTab(tab) {
this._defaultTab = tab || DEFAULT_ACTIVE_TAB;
this.activeTab = this.defaultTab;
}
get defaultTab() {
return this._defaultTab;
}
/**
* Sets the size of the component.
*
* The possible values are:
* * `small`
* * `medium` (Default)
* * `large`
* * `none`
*
*/
set size(size) {
this.renderer.removeClass(this.wrapper.nativeElement, getSizeClass('input', this.size));
this.renderer.removeClass(this.toggleButton.nativeElement, getSizeClass('button', this.size));
const newSize = size ? size : DEFAULT_SIZE;
if (newSize !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getSizeClass('input', newSize));
this.renderer.addClass(this.toggleButton.nativeElement, getSizeClass('button', newSize));
}
this._size = newSize;
}
get size() {
return this._size;
}
/**
* Sets the border radius of the component.
*
* The possible values are:
* * `small`
* * `medium` (Default)
* * `large`
* * `full`
* * `none`
*
*/
set rounded(rounded) {
this.renderer.removeClass(this.wrapper.nativeElement, getRoundedClass(this.rounded));
const newRounded = rounded ? rounded : DEFAULT_ROUNDED;
if (newRounded !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getRoundedClass(newRounded));
}
this._rounded = newRounded;
}
get rounded() {
return this._rounded;
}
/**
* Sets the fillMode of the component.
*
* The possible values are:
* * `solid` (Default)
* * `flat`
* * `outline`
* * `none`
*
*/
set fillMode(fillMode) {
this.renderer.removeClass(this.wrapper.nativeElement, getFillModeClass('input', this.fillMode));
this.renderer.removeClass(this.toggleButton.nativeElement, getFillModeClass('button', this.fillMode));
this.renderer.removeClass(this.toggleButton.nativeElement, `k-button-${this.fillMode}-base`);
const newFillMode = fillMode ? fillMode : DEFAULT_FILL_MODE;
if (newFillMode !== 'none') {
this.renderer.addClass(this.wrapper.nativeElement, getFillModeClass('input', newFillMode));
this.renderer.addClass(this.toggleButton.nativeElement, getFillModeClass('button', newFillMode));
this.renderer.addClass(this.toggleButton.nativeElement, `k-button-${newFillMode}-base`);
}
this._fillMode = newFillMode;
}
get fillMode() {
return this._fillMode;
}
/**
* @hidden
*/
get tabSwitchTransition() {
/*
When the popup is opening, disables the set transition in the themes. When `defaultTab` is set to `time`,
the popup opens with an active **Time** tab and the animation of the initial transition is undesired.
Setting the inline transition style to `none` overrides the set animation in the themes.
Setting the inline transition style to `null` does not apply any inline styles or override the themes CSS.
*/
return this.isOpen ? null : 'none';
}
/**
* @hidden
*
* Indicates whether the Calendar will be disabled.
* The inactive tab component gets disabled and becomes inaccessible on tab click.
*/
get disableCalendar() {
return this.activeTab !== 'date' && !this.calendar.isActive;
}
/**
* @hidden
*
* Indicates whether the TimeSelector will be disabled.
* The inactive tab component gets disabled and becomes inaccessible on tab click.
*/
get disableTimeSelector() {
return this.activeTab !== 'time' && !this.timeSelector.isActive;
}
/**
* @hidden
*/
get isAdaptiveModeEnabled() {
return this.adaptiveMode === 'auto';
}
/**
* @hidden
*/
get isAdaptive() {
return this.isAdaptiveModeEnabled && this.windowSize !== 'large';
}
/**
* @hidden
*/
onResize() {
const currentWindowSize = this.adaptiveService.size;
if (!this.isOpen || this.windowSize === currentWindowSize) {
return;
}
if (this.actionSheet.expanded) {
this.toggleActionSheet(false);
}
else {
this._togglePopup(false);
}
this.windowSize = currentWindowSize;
}
/**
* @hidden
*
* Controls whether the Calendar or the TimeSelector will be displayed.
*/
activeTab = DEFAULT_ACTIVE_TAB;
/**
* @hidden
*
* Specifies the stripped time-related format that is used in the TimeSelector.
* Updates each time the `format` property value changes.
*/
timeSelectorFormat = DEFAULT_TIMESELECTOR_FORMAT;
/**
* @hidden
*/
timeSelectorMin = cloneDate(MIN_TIME);
/**
* @hidden
*/
timeSelectorMax = cloneDate(MAX_TIME);
/**
* @hidden
*/
calendarValue = null;
/**
* @hidden
*/
calendarMin = cloneDate(MIN_DATE);
/**
* @hidden
*/
calendarMax = lastMillisecondOfDate(MAX_DATE);
/**
* @hidden
*/
checkIcon = checkIcon;
/**
* @hidden
*/
windowSize;
/**
* @hidden
*/
cellTemplate;
/**
* @hidden
*/
monthCellTemplate;
/**
* @hidden
*/
yearCellTemplate;
/**
* @hidden
*/
decadeCellTemplate;
/**
* @hidden
*/
centuryCellTemplate;
/**
* @hidden
*/
weekNumberTemplate;
/**
* @hidden
*/
headerTitleTemplate;
/**
* @hidden
*/
headerTemplate;
/**
* @hidden
*/
set headerTemplateRef(template) {
this.headerTemplate = template;
}
/**
* @hidden
*/
footerTemplate;
/**
* @hidden
*/
set footerTemplateRef(template) {
this.footerTemplate = template;
}
/**
* Toggles the visibility of the Calendar footer.
* @default false
*/
footer = false;
get activeTabComponent() {
if (!this.isOpen) {
return;
}
if (!(isPresent(this.calendar) || isPresent(this.timeSelector))) {
this.cdr.detectChanges();
}
return this.activeTab === 'date' ? this.calendar : this.timeSelector;
}
get appendTo() {
const { appendTo } = this.popupSettings;
if (!isPresent(appendTo) || appendTo === 'root') {
return undefined;
}
return appendTo === 'component' ? this.container : appendTo;
}
container;
popupTemplate;
actionSheet;
get popupUID() {
return this.calendar?.popupId;
}
get acceptButton() {
if (this.isAdaptive) {
return this.actionSheet.element.nativeElement.querySelector(ACCEPT_BUTTON_SELECTOR);
}
else {
return this.popupRef?.popup.instance.container.nativeElement.querySelector(ACCEPT_BUTTON_SELECTOR);
}
}
get cancelButtonElement() {
if (this.isAdaptive) {
return this.actionSheet.element.nativeElement.querySelector(CANCEL_BUTTON_SELECOTR);
}
else {
return this.popupRef?.popup.instance.container.nativeElement.querySelector(CANCEL_BUTTON_SELECOTR);
}
}
get dateTabButton() {
if (this.isAdaptive) {
return this.actionSheet.element.nativeElement.querySelector(DATE_TAB_BUTTON_SELECTOR);
}
else {
return this.popupRef?.popup.instance.container.nativeElement.querySelector(DATE_TAB_BUTTON_SELECTOR);
}
}
get timeTabButton() {
if (this.isAdaptive) {
return this.actionSheet.element.nativeElement.querySelector(TIME_TAB_BUTTON_SELECTOR);
}
else {
return this.popupRef?.popup.instance.container.nativeElement.querySelector(TIME_TAB_BUTTON_SELECTOR);
}
}
get todayButton() {
if (this.isAdaptive) {
return this.actionSheet.element.nativeElement.querySelector(TODAY_BUTTON_SELECTOR);
}
else {
return this.popupRef?.popup.instance.container.nativeElement.querySelector(TODAY_BUTTON_SELECTOR);
}
}
/**
* @hidden
*/
get formControl() {
const ngControl = this.injector.get(NgControl, null);
return ngControl?.control || null;
}
/**
* @hidden
*/
get isControlRequired() {
return isControlRequired(this.formControl);
}
popupRef;
_popupSettings = { animate: true };
_value = null;
_format = DEFAULT_DATEINPUT_FORMAT;
_tabindex = 0;
_defaultTab = DEFAULT_ACTIVE_TAB;
_min = mergeDateAndTime(MIN_DATE, MIN_TIME);
_max = mergeDateAndTime(MAX_DATE, MAX_TIME);
_disabledDates;
_isActive = false;
onControlTouched = noop;
onControlChange = noop;
onValidatorChange = noop;
minValidateFn = noop;
maxValidateFn = noop;
disabledDatesValidateFn = noop;
incompleteValidator = noop;
subscriptions = new Subscription();
ariaActiveDescendantSubscription;
_size = DEFAULT_SIZE;
_rounded = DEFAULT_ROUNDED;
_fillMode = DEFAULT_FILL_MODE;
constructor(popupService, intl, cdr, pickerService, ngZone, wrapper, localization, disabledDatesService, renderer, injector, adaptiveService) {
super();
this.popupService = popupService;
this.intl = intl;
this.cdr = cdr;
this.pickerService = pickerService;
this.ngZone = ngZone;
this.wrapper = wrapper;
this.localization = localization;
this.disabledDatesService = disabledDatesService;
this.renderer = renderer;
this.injector = injector;
this.adaptiveService = adaptiveService;
validatePackage(packageMetadata);
}
ngOnInit() {
this.subscriptions.add(this.pickerService.onFocus
// detect popup changes to disable the inactive view mark-up when the popup is open
.pipe(tap(this.detectPopupChanges.bind(this)))
.subscribe(this.handleFocus.bind(this)));
this.subscriptions.add(this.pickerService.onBlur.subscribe(this.handleBlur.bind(this)));
this.subscriptions.add(this.pickerService.sameDateSelected.subscribe(this.handleCalendarValueChange.bind(this)));
this.subscriptions.add(this.localization.changes.subscribe(() => this.cdr.markForCheck()));
this.subscriptions.add(this.pickerService.dateCompletenessChange.subscribe(this.handleDateCompletenessChange.bind(this)));
if (isWindowAvailable()) {
this.subscriptions.add(this.ngZone.runOutsideAngular(() => fromEvent(window, 'blur').subscribe(() => {
if (!this.isAdaptive) {
this.handleCancel();
}
})));
}
this.focusableId = this.dateInput?.focusableId;
this.minValidateFn = this.rangeValidation ? minValidator(this.min) : noop;
this.maxValidateFn = this.rangeValidation ? maxValidator(this.max) : noop;
}
ngAfterViewInit() {
this.setComponentClasses();
this.windowSize = this.adaptiveService.size;
}
ngOnChanges(changes) {
if (isPresent(changes['min']) || isPresent(changes['max'])) {
this.verifyMinMaxRange();
}
if (changes['min'] || changes['max'] || changes['rangeValidation'] || changes['disabledDatesValidation'] || changes['disabledDates'] || changes['incompleteDateValidation']) {
this.minValidateFn = this.rangeValidation ? minValidator(this.min) : noop;
this.maxValidateFn = this.rangeValidation ? maxValidator(this.max) : noop;
this.disabledDatesValidateFn = this.disabledDatesValidation ? disabledDatesValidator(this.disabledDatesService.isDateDisabled) : noop;
this.incompleteValidator = this.incompleteDateValidation ? incompleteDateValidator() : noop;
this.onValidatorChange();
}
if (!this.focusableId || changes['focusableId']) {
this.focusableId = this.dateInput?.focusableId;
}
}
ngOnDestroy() {
if (this.isOpen && !this.isAdaptive) {
this.closePopup();
}
this.subscriptions.unsubscribe();
}
/**
* * If the popup is closed, focuses the DateTimePicker input.
* * If the popup is open, the focus is moved to its content.
*/
focus() {
if (this.disabled) {
return;
}
if (this.isOpen) {
this.activeTabComponent.focus();
}
else {
this.dateInput.focus();
}
}
/**
* Blurs the DateTimePicker.
*/
blur() {
if (this.isOpen && this.activeTabComponent.isActive) {
this.activeTabComponent.blur();
}
else {
this.dateInput.blur();
}
}
/**
* Toggles the visibility of the popup or actionSheet.
* If you use the `toggle` method to show or hide the popup or actionSheet,
* the `open` and `close` events do not fire.
*
* @param show - The state of the popup.
*/
toggle(show) {
if (this.disabled || this.readonly || show === this.isOpen) {
return;
}
const previousWindowSize = this.windowSize;
this.windowSize = this.adaptiveService.size;
if (previousWindowSize !== this.windowSize && !show) {
if (previousWindowSize !== 'large') {
this.toggleActionSheet(show);
}
else {
this._togglePopup(show);
}
}
else {
if (this.isAdaptive) {
this.toggleActionSheet(show);
}
else {
this._togglePopup(show);
}
}
}
/**
* @hidden
*/
writeValue(value) {
this.verifyValue(value);
this.value = cloneDate(value);
this.cdr.markForCheck();
if (!value && this.dateInput) {
this.dateInput.placeholder = this.placeholder;
this.dateInput.writeValue(value);
}
}
/**
* @hidden
*/
registerOnChange(fn) {
this.onControlChange = fn;
}
/**
* @hidden
*/
registerOnTouched(fn) {
this.onControlTouched = fn;
}
/**
* @hidden
*/
setDisabledState(disabled) {
this.disabled = disabled;
this.cdr.markForCheck();
}
/**
* @hidden
*/
validate(control) {
return this.minValidateFn(control) || this.maxValidateFn(control) || this.disabledDatesValidateFn(control) || this.incompleteValidator(control, this.dateInput && this.dateInput.isDateIncomplete);
}
/**
* @hidden
*/
registerOnValidatorChange(fn) {
this.onValidatorChange = fn;
}
/**
* @hidden
*
* Used by the TextBoxContainer to determine if the floating label will render in the input.
*/
isEmpty() {
return !isPresent(this.value) && this.dateInput.isEmpty();
}
/**
* @hidden
*/
handleIconClick(event) {
if (this.disabled || this.readonly) {
return;
}
// prevents the event default to evade focusing the DateInput input when placed inside a label (FF/IE/Edge)
event.preventDefault();
const runInZone = !this.isOpen || hasObservers(this.close);
this.run(runInZone, () => {
const shouldOpen = !this.isOpen;
// handle focus first to maintain correct event order `focus` => `open`
this.handleFocus();
if (!shouldOpen) {
this.dateInput.focus();
}
this.toggleDateTime(shouldOpen);
this.switchFocus();
});
}
/**
* @hidden
*/
handleFocus() {
if (this.isActive) {
return;
}
this.isActive = true;
if (hasObservers(this.onFocus)) {
this.ngZone.run(() => this.onFocus.emit());
}
}
/**
* @hidden
*/
handleBlur(event) {
if (!this.isActive || this.focusTargetInComponent(event)) {
return;
}
this.isActive = false;
const isNgControlUntouched = this.wrapper.nativeElement.classList.contains('ng-untouched');
const runInZone = isNgControlUntouched || hasObservers(this.onBlur) || (this.isOpen && hasObservers(this.close));
this.run(runInZone, () => {
this.onBlur.emit();
this.onControlTouched();
this.toggleDateTime(false);
this.cdr.markForCheck();
});
}
/**
* @hidden
*/
changeActiveTab(tab) {
if (!this.isOpen || this.activeTab === tab) {
return;
}
// persists the Tcurrent value of the TimeSelector when switching between tabs
if (!isEqual(this.timeSelector.value, this.timeSelector.current)) {
this.timeSelector.handleAccept();
}
this.activeTab = tab;
this.cdr.detectChanges();
this.detectPopupChanges();
}
/**
* @hidden
*/
handleTabChangeTransitionEnd(dateTimeSelector, event) {
// handle only the .k-datetime-selector element transition, ignore any child element transitions
if (event.target !== dateTimeSelector) {
return;
}
if (this.activeTab === 'time') {
this.renderer.removeAttribute(this.dateInput?.inputElement, attributeNames.ariaActiveDescendant);
}
this.activeTabComponent.focus();
}
/**
* @hidden
*/
onTabOutLastPart() {
if (this.calendarValue) {
this.acceptButton.focus();
}
else if (!this.calendarValue && this.cancelButton) {
this.cancelButtonElement.focus();
}
else {
this.dateTabButton.focus();
}
}
/**
* @hidden
*/
onTabOutFirstPart() {
if (this.activeTab === 'time') {
this.renderer.removeClass(this.timeSelector.timeListWrappers.first.nativeElement, 'k-focus');
this.timeSelector.showNowButton ? this.timeSelector.now.nativeElement.focus() : this.timeTabButton.focus();
}
}
/**
* @hidden
*/
onTabOutNow() {
this.timeTabButton.focus();
}
/**
* @hidden
*/
handleAccept() {
if (!this.isOpen) {
return;
}
const candidate = mergeDateAndTime(this.calendar.value, this.timeSelector.current);
const valueChangePresent = !isEqual(this.value, candidate);
const runInZone = valueChangePresent || hasObservers(this.close);
this.run(runInZone, () => {
this.handleValueChange(candidate);
this.dateInput.focus();
this.toggleDateTime(false);
});
}
/**
* @hidden
*/
handleCancel() {
if (!this.isOpen) {
return;
}
const runInZone = hasObservers(this.close);
this.dateInput.focus();
this.run(runInZone, () => this.toggleDateTime(false));
hasObservers(this.escape) && this.escape.emit();
}
/**
* @hidden
*/
handleInputValueChange(value) {
this.handleValueChange(value);
if (this.isOpen) {
this.toggleDateTime(false);
}
}
/**
* @hidden
*/
handleDateInputClick() {
this.windowSize = this.adaptiveService.size;
if (this.isAdaptive) {
this.toggleDateTime(true);
}
}
/**
* @hidden
*/
handleCalendarValueChange() {
this.setTimeSelectorMinMax(this.calendar.value);
this.changeActiveTab('time');
}
/**
* @hidden
*/
handleKeyDown(event) {
if (this.disabled || this.readonly) {
return;
}
const { keyCode, altKey } = event;
switch (keyCode) {
case altKey && Keys.ArrowUp:
case Keys.Escape:
this.handleCancel();
break;
case !this.isOpen && altKey && Keys.ArrowDown:
this.ngZone.run(() => this.toggleDateTime(true));
break;
case altKey && Keys.ArrowRight:
this.changeActiveTab('time');
break;
case altKey && Keys.ArrowLeft:
this.changeActiveTab('date');
break;
case this.isOpen && this.timeSelector.isActive && isPresent(this.calendarValue) && Keys.Enter:
this.handleAccept();
break;
default: return;
}
}
/**
* @hidden
*/
handleTab(event) {
event.preventDefault();
const { shiftKey } = event;
switch (event.target) {
case this.acceptButton:
if (!shiftKey) {
this.cancelButton ? this.cancelButtonElement.focus() : this.dateTabButton.focus();
}
else {
if (this.activeTab === 'date') {
this.calendar.monthView.list.nativeElement.focus();
}
else {
this.timeSelector.timeLists.last.focus();
}
}
break;
case this.cancelButtonElement:
if (this.calendarValue) {
shiftKey ? this.acceptButton.focus() : this.dateTabButton.focus();
}
else {
if (shiftKey) {
if (this.activeTab === 'date') {
this.calendar.monthView.list.nativeElement.focus();
}
else {
this.timeSelector.timeLists.last.focus();
}
}
else {
this.dateTabButton.focus();
}
}
break;
case this.dateTabButton:
if (this.cancelButton) {
this.cancelButtonElement.focus();
}
if (!this.cancelButton && this.calendarValue) {
this.acceptButton.focus();
}
if (!this.cancelButton && !this.calendarValue) {
if (this.activeTab === 'date') {
this.calendar.monthView.list.nativeElement.focus();
}
else {
this.timeSelector.timeLists.last.focus();
}
}
break;
case this.timeTabButton:
if (this.activeTab === 'time') {
this.timeSelector.showNowButton ?
this.timeSelector.now.nativeElement.focus() :
this.timeSelector.timeLists.first.focus();
}
else {
this.todayButton.focus();
}
break;
default:
break;
}
}
/**
* @hidden
*/
handleActionSheetCollapse() {
// If not handled, the actionsheet expanded state does not get updated when overlay is clicked
this.cdr.markForCheck();
// Reset tabs after actionsheet is collapsed, otherwise the tab change can be seen during the animation
this.resetActiveTab();
}
/**
* @hidden
*
* Prevents the diversion of the focus from the currently active element in the component.
*/
preventMouseDown(event) {
event.preventDefault();
}
/**
* @hidden
*/
toggleActionSheet(show) {
if (isPresent(show) && show && !this.isOpen) {
this.actionSheet.toggle();
this.renderer.setAttribute(this.actionSheet.element.nativeElement, 'id', this.popupUID);
this.renderer.setAttribute(this.dateInput?.inputElement, attributeNames.ariaControls, this.popupUID);
this.setAriaActiveDescendant();
}
else if (isPresent(show) && !show && this.isOpen) {
this.handleBlur();
this.actionSheet.toggle();
this.ariaActiveDescendantSubscription.unsubscribe();
if (this.dateInput) {
this.renderer.removeAttribute(this.dateInput.inputElement, attributeNames.ariaActiveDescendant);
this.renderer.removeAttribute(this.dateInput.inputElement, attributeNames.ariaControls);
this.dateInput.focus();
}
}
}
_togglePopup(show) {
const shouldOpen = isPresent(show) ? show : !this.isOpen;
if (shouldOpen) {
this.openPopup();
return;
}
this.closePopup();
this.resetActiveTab();
}
/**
* Changes the tab and the calendar or clock icon to the designated default.
*/
resetActiveTab() {
if (this.activeTab !== this.defaultTab) {
this.activeTab = this.defaultTab;
this.cdr.detectChanges();
}
}
verifyValue(value) {
if (!isDevMode()) {
return;
}
if (isPresent(value) && !(value instanceof Date)) {
throw new Error(`The 'value' should be a valid JavaScript Date instance or null. Check ${VALUE_DOC_LINK} for possible resolution.`);
}
}
verifyMinMaxRange() {
if (!isDevMode()) {
return;
}
if (!isValidRange(this.min, this.max)) {
throw new Error(`The max value should be bigger than the min. See ${MIN_MAX_DOC_LINK}.`);
}
}
/**
* Extracts the time slots and the literals that are not preceded by date parts
* and concatenates the resulting parts into a string.
* If the provided format value does not contain any time parts,
* returns the designated format of the default popup component of the TimePicker.
*/
getTimeSelectorFormat(format) {
const timeSelectorFormat = this.intl
.splitDateFormat(format)
.filter(this.timeFormatPartFilter)
.reduce((format, part) => format += part.pattern, '');
return timeSelectorFormat || DEFAULT_TIMESELECTOR_FORMAT;
}
/**
* Extracts the `displayFormat` from the provided `string | FormatSettings` value.
* Fallbacks to the default input value, if a falsy value param is passed.
*/
getDisplayFormat(format) {
if (!format) {
return DEFAULT_DATEINPUT_FORMAT;
}
if (typeof format === 'string') {
return format;
}
else {
return format.displayFormat;
}
}
/**
* The filter expression that filters out all format parts
* except for `hour`, `minute`, `second`, `dayperiod`, and specific literals.
* Literals will be left only if they are not preceded by date parts.
*/
timeFormatPartFilter(part, index, parts) {
const previousPart = index >= 1 && parts[index - 1];
if (previousPart && part.type === 'literal') {
return timeFormatRegExp.test(previousPart.type);
}
return timeFormatRegExp.test(part.type);
}
/**
* @hidden
*/
toggleDateTime(open) {
if (open === this.isOpen) {
return;
}
const event = new PreventableEvent();
if (open) {
this.open.emit(event);
}
else {
this.close.emit(event);
}
if (event.isDefaultPrevented()) {
return;
}
this.toggle(open);
this.switchFocus();
}
switchFocus() {
if (!this.isActive) {
return;
}
if (this.isOpen) {
this.activeTabComponent.focus();
}
else if (!touchEnabled) {
this.dateInput.focus();
}
else if (!this.dateInput.isActive) {
this.handleBlur();
}
}
openPopup() {
this.setCalendarValue(this.value);
this.setTimeSelectorMinMax(this.value);
const direction = this.localization.rtl ? 'right' : 'left';
const appendToComponent = typeof this.popupSettings.appendTo === 'string' && this.popupSettings.appendTo === 'component';
this.popupRef = this.popupService.open({
anchor: this.wrapper,
content: this.popupTemplate,
positionMode: appendToComponent ? 'fixed' : 'absolute',
animate: this.popupSettings.animate,
appendTo: this.appendTo,
popupClass: `k-datetime-container k-datetimepicker-popup ${this.popupSettings.popupClass || ''}`,
anchorAlign: { vertical: 'bottom', horizontal: direction },
popupAlign: { vertical: 'top', horizontal: direction }
});
this.popupRef.popupElement.setAttribute('id', this.popupUID);
this.renderer.setAttribute(this.dateInput?.inputElement, attributeNames.ariaControls, this.popupUID);
this.setAriaActiveDescendant();
this.popupRef.popupAnchorViewportLeave.subscribe(() => this.handleCancel());
if (this.calendar.type === 'infinite') {
this.subscriptions.add(fromEvent(this.calendar.monthView.list.nativeElement, 'keydown').subscribe((event) => {
const { keyCode, shiftKey } = event;
if (keyCode === Keys.Tab && !shiftKey) {
event.preventDefault();
if (!this.calendarValue && !this.cancelButton) {
this.dateTabButton.focus();
}
else if (this.calendarValue) {
this.acceptButton.focus();
}
else if (this.cancelButton) {
this.cancelButtonElement.focus();
}
}
}));
}
}
setAriaActiveDescendant() {
const focusedCellChangeEvent = this.calendar.type === 'infinite' ?
this.calendar.monthView.focusedCellChange :
this.calendar.multiViewCalendar.viewList.focusedCellChange;
this.ariaActiveDescendantSubscription = focusedCellChangeEvent.subscribe((id) => this.renderer.setAttribute(this.dateInput?.inputElement, attributeNames.ariaActiveDescendant, id));
}
closePopup() {
if (!this.isOpen) {
return;
}
this.ariaActiveDescendantSubscription.unsubscribe();
if (this.dateInput) {
this.renderer.removeAttribute(this.dateInput?.inputElement, attributeNames.ariaControls);
this.renderer.removeAttribute(this.dateInput?.inputElement, attributeNames.ariaActiveDescendant);
}
this.popupRef.close();
this.popupRef = null;
}
handleValueChange(value) {
if (isEqual(this.