UNPKG

@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
/**----------------------------------------------------------------------------------------- * 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.