@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,297 lines • 70.9 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, ChangeDetectorRef, ChangeDetectionStrategy, ContentChild, EventEmitter, ElementRef, Renderer2, isDevMode, forwardRef, HostBinding, Input, Output, ViewChild, Optional, NgZone, Injector } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, NgControl } from '@angular/forms';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { cloneDate, isEqual } from '@progress/kendo-date-math';
import { hasObservers, KendoInput, guid, Keys, isObject, ResizeSensorComponent } from '@progress/kendo-angular-common';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { MultiViewCalendarComponent } from './multiview-calendar.component';
import { NavigationComponent } from './navigation.component';
import { ViewListComponent } from './view-list.component';
import { CalendarDOMService } from './services/dom.service';
import { BusViewService } from './services/bus-view.service';
import { NavigationService } from './services/navigation.service';
import { DisabledDatesService } from './services/disabled-dates.service';
import { SelectionService } from './services/selection.service';
import { ScrollSyncService } from './services/scroll-sync.service';
import { CellTemplateDirective } from './templates/cell-template.directive';
import { MonthCellTemplateDirective } from './templates/month-cell-template.directive';
import { YearCellTemplateDirective } from './templates/year-cell-template.directive';
import { DecadeCellTemplateDirective } from './templates/decade-cell-template.directive';
import { CenturyCellTemplateDirective } from './templates/century-cell-template.directive';
import { WeekNumberCellTemplateDirective } from './templates/weeknumber-cell-template.directive';
import { HeaderTitleTemplateDirective } from './templates/header-title-template.directive';
import { NavigationItemTemplateDirective } from './templates/navigation-item-template.directive';
import { PickerService } from '../common/picker.service';
import { CalendarViewEnum } from './models/view.enum';
import { handleRangeSelection } from './models/selection';
import { minValidator } from '../validators/min.validator';
import { maxValidator } from '../validators/max.validator';
import { MIN_DATE, MAX_DATE } from '../defaults';
import { areDatesEqual, dateInRange, DEFAULT_SIZE, getSizeClass, getToday, hasExistingValue, last, noop } from '../util';
import { closest } from '../common/dom-queries';
import { requiresZoneOnBlur, preventDefault, isPresent, isArrowWithShiftPressed, selectors, attributeNames, isNullOrDate } from '../common/utils';
import { from as fromPromise } from 'rxjs';
import { HeaderTemplateDirective } from './templates/header-template.directive';
import { FooterTemplateDirective } from './templates/footer-template.directive';
import { MultiViewCalendarCustomMessagesComponent } from './localization/multiview-calendar-custom-messages.component';
import { NgIf } from '@angular/common';
import { CalendarLocalizedMessagesDirective } from './localization/calendar-localized-messages.directive';
import * as i0 from "@angular/core";
import * as i1 from "./services/bus-view.service";
import * as i2 from "./services/dom.service";
import * as i3 from "./services/navigation.service";
import * as i4 from "./services/scroll-sync.service";
import * as i5 from "./services/disabled-dates.service";
import * as i6 from "@progress/kendo-angular-l10n";
import * as i7 from "./services/selection.service";
import * as i8 from "../common/picker.service";
const BOTTOM_VIEW_DOC_LINK = 'https://www.telerik.com/kendo-angular-ui/components/dateinputs/api/CalendarComponent/#toc-bottomview';
const TOP_VIEW_DOC_LINK = 'https://www.telerik.com/kendo-angular-ui/components/dateinputs/api/CalendarComponent/#toc-topview';
const MIN_DOC_LINK = 'https://www.telerik.com/kendo-angular-ui/components/dateinputs/api/CalendarComponent/#toc-min';
const MAX_DOC_LINK = 'https://www.telerik.com/kendo-angular-ui/components/dateinputs/api/CalendarComponent/#toc-max';
const VALUE_DOC_LINK = 'https://www.telerik.com/kendo-angular-ui/components/dateinputs/calendar/#toc-using-with-json';
const virtualizationProp = x => x ? x.virtualization : null;
/**
* @hidden
*/
export const CALENDAR_VALUE_ACCESSOR = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CalendarComponent)
};
/**
* @hidden
*/
export const CALENDAR_RANGE_VALIDATORS = {
multi: true,
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CalendarComponent)
};
/**
* @hidden
*/
export const KENDO_INPUT_PROVIDER = {
provide: KendoInput,
useExisting: forwardRef(() => CalendarComponent)
};
/**
* Represents the [Kendo UI Calendar component for Angular](slug:overview_calendar).
* @example
* ```html
* <kendo-calendar></kendo-calendar>
* ```
*
* @remarks
* Supported children components are: {@link CalendarCustomMessagesComponent}.
*/
export class CalendarComponent {
bus;
dom;
element;
navigator;
renderer;
cdr;
ngZone;
injector;
scrollSyncService;
disabledDatesService;
localization;
selectionService;
pickerService;
/**
* Shows days that fall outside the current month and the default values per Calendar type are false for infinite and true for classic ([see example]({% slug viewoptions_calendar %}#toc-displaying-other-month-days)).
*/
set showOtherMonthDays(_showOtherMonthDays) {
this._showOtherMonthDays = _showOtherMonthDays;
}
get showOtherMonthDays() {
if (this._showOtherMonthDays === undefined) {
return this.type === 'classic';
}
return this._showOtherMonthDays;
}
_showOtherMonthDays;
/**
* @hidden
*/
id;
/**
* @hidden
*/
get popupId() {
return `kendo-popup-${this.bus.calendarId}`;
}
/**
* Specifies the focused date of the Calendar
* ([see example]({% slug dates_calendar %}#toc-focused-dates)).
*
* If the Calendar is outside the `min` or `max` range, the component normalizes the defined `focusedDate`.
*/
set focusedDate(focusedDate) {
if (this.activeViewDate && !isEqual(this._focusedDate, focusedDate)) {
const service = this.bus.service(this.activeViewEnum);
const lastDayInPeriod = service.lastDayOfPeriod(this.activeViewDate);
const isFocusedDateInRange = service.isInRange(focusedDate, this.activeViewDate, lastDayInPeriod);
if (!isFocusedDateInRange) {
this.emitNavigate(focusedDate);
}
}
this._focusedDate = focusedDate || getToday();
this.setAriaActivedescendant();
}
get focusedDate() {
if (this._focusedDate > this.max) {
return this.max;
}
if (this._focusedDate < this.min) {
return this.min;
}
return this._focusedDate;
}
/**
* @hidden
*/
get headerId() {
return this.id + '-header';
}
/**
* Specifies the minimum allowed date value
* ([see example]({% slug dateranges_calendar %})).
*
* @default 1900-1-1
*/
set min(min) {
this._min = min || new Date(MIN_DATE);
}
get min() {
return this._min;
}
/**
* Specifies the maximum allowed date value
* ([see example]({% slug dateranges_calendar %})).
*
* @default 2099-12-31
*/
set max(max) {
this._max = max || new Date(MAX_DATE);
}
get max() {
return this._max;
}
/**
* Determines whether the built-in `min` or `max` validators are enforced when validating a form.
*
* @default false
*/
rangeValidation = false;
/**
* Specifies the format of the displayed week day names.
*
* @default 'short'
*/
weekDaysFormat = "short";
/**
* Toggles the visibility of the footer.
*
* @default false
*/
footer = false;
/**
* Sets the Calendar selection mode
* ([see example]({% slug selection_calendar %})).
* @default 'single'
*/
set selection(_selection) {
this._selection = _selection;
this.selectionSetter = true;
}
get selection() {
return this._selection;
}
_selection = 'single';
/**
* Allows reverse selection when using `range` selection.
* If `allowReverse` is set to `true`, the component skips the validation of whether the start date is after the end date.
*
* @default false
*/
allowReverse = false;
/**
* Sets or gets the `value` property of the Calendar and defines the selected value of the component.
*
* The `value` has to be a valid [JavaScript `Date`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Date) instance when in `single` selection mode, an array of valid JavaScript Date instances when in `multiple` selection mode, or an object of type `SelectionRange` when in `range` selection mode.
*/
set value(candidate) {
this.valueSetter = true;
this._value = candidate;
}
get value() {
return this._value;
}
/**
* Sets or gets the `disabled` property of the Calendar and
* determines whether the component is active
* ([see example]({% slug disabled_calendar %})).
* To learn how to disable the component in reactive forms, refer to the article on [Forms Support](slug:formssupport_calendar#toc-managing-the-calendar-disabled-state-in-reactive-forms).
*/
disabled = false;
/**
* Specifies the `tabindex` property of the Calendar. Based on the
* [HTML `tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) behavior,
* it determines whether the component is focusable.
*
* @default 0
*/
tabindex = 0;
/**
* @hidden
*/
set tabIndex(tabIndex) {
this.tabindex = tabIndex;
}
get tabIndex() {
return this.tabindex;
}
/**
* Specifies the dates of the Calendar that will be disabled
* ([see example]({% slug disabled_dates_calendar %})).
*/
set disabledDates(value) {
this.disabledDatesService.initialize(value);
this._disabledDates = value;
}
get disabledDates() {
return this._disabledDates;
}
/**
* Determines whether the navigation side-bar will be displayed
* ([see example]({% slug sidebar_calendar %})).
* Applies to the [`infinite`]({% slug api_dateinputs_calendarcomponent %}#toc-type) Calendar only.
*
* @default true
*/
navigation = true;
/**
* Defines the active view that the Calendar initially renders
* ([see example]({% slug viewoptions_calendar %})).
* You have to set `activeView` within the `topView`-`bottomView` range.
*
* @default 'month'
*/
activeView = CalendarViewEnum[CalendarViewEnum.month];
/**
* Defines the bottommost view to which the user can navigate
* ([see example](slug:viewdepth_calendar)).
*
* @default 'month'
*/
bottomView = CalendarViewEnum[CalendarViewEnum.month];
/**
* Defines the topmost view to which the user can navigate
* ([see example](slug:viewdepth_calendar)).
*
* @default 'century'
*/
topView = CalendarViewEnum[CalendarViewEnum.century];
/**
* Specifies the Calendar type.
*
* @default 'infinite'
*/
set type(type) {
this.renderer.removeClass(this.element.nativeElement, `k-calendar-${this.type}`);
if (type === 'infinite') {
this.renderer.addClass(this.element.nativeElement, `k-calendar-${type}`);
}
this._type = type;
}
get type() {
return this._type;
}
/**
* Determines whether to enable animation when navigating to previous/next view.
* Applies to the [`classic`]({% slug api_dateinputs_calendarcomponent %}#toc-type) 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
*/
animateNavigation = false;
/**
* Determines whether to display a week number column in the `month` view
* ([see example]({% slug weeknumcolumn_calendar %})).
*
* @default false
*/
weekNumber = false;
/**
* @hidden
*/
closePopup = new EventEmitter();
/**
* Fires when the active view is changed
* ([see example](slug:events_calendar)).
*/
activeViewChange = new EventEmitter();
/**
* Fires when navigating in the currently active view
* ([see example](slug:events_calendar)).
*/
navigate = new EventEmitter();
/**
* Fires when the active view date is changed
* ([see example](slug:events_calendar)).
* Applies to the [`infinite`]({% slug api_dateinputs_calendarcomponent %}#toc-type) Calendar only.
*/
activeViewDateChange = new EventEmitter();
/**
* Fires each time the Calendar gets blurred
* ([see example](slug:events_calendar)).
*/
onBlur = new EventEmitter();
/**
* Fires each time the Calendar gets focused
* ([see example](slug:events_calendar)).
*/
onFocus = new EventEmitter();
/**
* Fires when the value is changed
* ([see example](slug:events_calendar)).
*/
valueChange = new EventEmitter();
/**
* @hidden
*
* Queries the template for a cell template declaration.
* Ignored if a `[cellTemplate]` value is explicitly provided.
*/
cellTemplate;
/**
* @hidden
*
* Defines the template for each cell.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set cellTemplateRef(template) {
this._cellTemplateRef = template;
}
get cellTemplateRef() {
return this._cellTemplateRef || this.cellTemplate;
}
/**
* @hidden
*
* Queries the template for a month cell template declaration.
* Ignored if a `[monthCellTemplate]` value is explicitly provided.
*/
monthCellTemplate;
/**
* @hidden
*
* Defines the template for each month cell.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set monthCellTemplateRef(template) {
this._monthCellTemplateRef = template;
}
get monthCellTemplateRef() {
return this._monthCellTemplateRef || this.monthCellTemplate;
}
/**
* @hidden
*
* Queries the template for a year cell template declaration.
* Ignored if a `[yearCellTemplate]` value is explicitly provided.
*/
yearCellTemplate;
/**
* @hidden
*
* Defines the template for each year cell.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set yearCellTemplateRef(template) {
this._yearCellTemplateRef = template;
}
get yearCellTemplateRef() {
return this._yearCellTemplateRef || this.yearCellTemplate;
}
/**
* @hidden
*
* Queries the template for a decade cell template declaration.
* Ignored if a `[decadeCellTemplate]` value is explicitly provided.
*/
decadeCellTemplate;
/**
* @hidden
*
* Defines the template for each decade cell.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set decadeCellTemplateRef(template) {
this._decadeCellTemplateRef = template;
}
get decadeCellTemplateRef() {
return this._decadeCellTemplateRef || this.decadeCellTemplate;
}
/**
* @hidden
*
* Queries the template for a century cell template declaration.
* Ignored if a `[centuryCellTemplate]` value is explicitly provided.
*/
centuryCellTemplate;
/**
* @hidden
*
* Defines the template for each century cell.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set centuryCellTemplateRef(template) {
this._centuryCellTemplateRef = template;
}
get centuryCellTemplateRef() {
return this._centuryCellTemplateRef || this.centuryCellTemplate;
}
/**
* @hidden
*
* Queries the template for a week number cell template declaration.
* Ignored if a `[weekNumberTemplate]` value is explicitly provided.
*/
weekNumberTemplate;
/**
* @hidden
*
* Defines the template for the week cell.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set weekNumberTemplateRef(template) {
this._weekNumberTemplateRef = template;
}
get weekNumberTemplateRef() {
return this._weekNumberTemplateRef || this.weekNumberTemplate;
}
/**
* @hidden
*
* Queries the template for a header title template declaration.
* Ignored if a `[headerTitleTemplate]` value is explicitly provided.
*/
headerTitleTemplate;
/**
* @hidden
*
* Queries the template for a header template declaration.
* Ignored if a `[headerTemplate]` value is explicitly provided.
*/
headerTemplate;
/**
* @hidden
*
* Queries the template for a footer template declaration.
* Ignored if a `[footerTemplate]` value is explicitly provided.
*/
footerTemplate;
/**
* @hidden
*
* Defines the template for the header title.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set headerTitleTemplateRef(template) {
this._headerTitleTemplateRef = template;
}
get headerTitleTemplateRef() {
return this._headerTitleTemplateRef || this.headerTitleTemplate;
}
/**
* @hidden
*
* Defines the template for the header.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set headerTemplateRef(template) {
this._headerTemplateRef = template;
}
get headerTemplateRef() {
return this._headerTemplateRef || this.headerTemplate;
}
/**
* @hidden
*
* Defines the template for the footer.
*/
set footerTemplateRef(template) {
this._footerTemplateRef = template;
}
get footerTemplateRef() {
return this._footerTemplateRef || this.footerTemplate;
}
/**
* @hidden
*
* Queries the template for a navigation item template declaration.
* Ignored if a `[navigationItemTemplate]` value is explicitly provided.
*/
navigationItemTemplate;
/**
* @hidden
*
* Defines the template for the navigation item.
* Takes precedence over nested templates in the KendoCalendar tag.
*/
set navigationItemTemplateRef(template) {
this._navigationItemTemplateRef = template;
}
get navigationItemTemplateRef() {
return this._navigationItemTemplateRef || this.navigationItemTemplate;
}
/**
* @hidden
*
* TODO: Make visible when true sizing of all calendar elements is implemented
* Sets the size of the component.
*
* The possible values are:
* * `small`
* * `medium` (Default)
* * `large`
* * `none`
*
*/
set size(size) {
this._size = size;
}
get size() {
return this._size;
}
_size = DEFAULT_SIZE;
/**
* Specifies which end of the defined selection range should be marked as active.
* Value will be ignored if the selection range is undefined. If range selection is used then the default value is 'start'.
*
* @default 'start'
*/
set activeRangeEnd(_activeRangeEnd) {
this._activeRangeEnd = _activeRangeEnd;
}
get activeRangeEnd() {
return this._activeRangeEnd;
}
_activeRangeEnd = 'start';
navigationView;
monthView;
multiViewCalendar;
isActive = false;
cellUID = guid();
selectionRange = { start: null, end: null };
selectedDates = [];
rangePivot;
_disabledDates;
_min = new Date(MIN_DATE);
_max = new Date(MAX_DATE);
_focusedDate = getToday();
_value;
onControlChange = noop;
onControlTouched = noop;
onValidatorChange = noop;
minValidateFn = noop;
maxValidateFn = noop;
changes = {};
valueSetter = false;
selectionSetter = false;
syncNavigation = true;
viewChangeSubscription;
_type = 'infinite';
_cellTemplateRef;
_monthCellTemplateRef;
_yearCellTemplateRef;
_decadeCellTemplateRef;
_centuryCellTemplateRef;
_weekNumberTemplateRef;
_headerTitleTemplateRef;
_headerTemplateRef;
_footerTemplateRef;
_navigationItemTemplateRef;
get activeViewEnum() {
const activeView = CalendarViewEnum[this.activeView];
return activeView < this.bottomViewEnum ? this.bottomViewEnum : activeView;
}
get bottomViewEnum() {
return CalendarViewEnum[this.bottomView];
}
get topViewEnum() {
return CalendarViewEnum[this.topView];
}
get widgetId() {
return this.id;
}
get ariaDisabled() {
// in Classic mode, the inner MultiViewCalendar should handle the disabled class and aria attr
return this.type === 'classic' ? undefined : this.disabled;
}
domEvents = [];
control;
pageChangeSubscription;
resolvedPromise = Promise.resolve(null);
destroyed = false;
localizationChangeSubscription;
activeViewDate;
currentlyFocusedElement;
canHover = false;
constructor(bus, dom, element, navigator, renderer, cdr, ngZone, injector, scrollSyncService, disabledDatesService, localization, selectionService, pickerService) {
this.bus = bus;
this.dom = dom;
this.element = element;
this.navigator = navigator;
this.renderer = renderer;
this.cdr = cdr;
this.ngZone = ngZone;
this.injector = injector;
this.scrollSyncService = scrollSyncService;
this.disabledDatesService = disabledDatesService;
this.localization = localization;
this.selectionService = selectionService;
this.pickerService = pickerService;
validatePackage(packageMetadata);
this.id = `kendo-calendarid-${this.bus.calendarId}`;
if (this.pickerService) {
this.pickerService.calendar = this;
}
}
ngOnInit() {
this.setClasses(this.element.nativeElement);
if (this.type === 'infinite') {
this.dom.calculateHeights(this.element.nativeElement);
this.scrollSyncService.configure(this.activeViewEnum);
}
this.localizationChangeSubscription = this.localization.changes.subscribe(() => this.cdr.markForCheck());
this.viewChangeSubscription = this.bus.viewChanged.subscribe(({ view }) => this.handleActiveViewChange(CalendarViewEnum[view]));
this.control = this.injector.get(NgControl, null);
if (this.element) {
this.ngZone.runOutsideAngular(() => {
this.bindEvents();
});
}
}
ngOnChanges(changes) {
this.changes = changes;
this.verifyChanges();
this.bus.configure(this.bottomViewEnum, this.topViewEnum);
this.scrollSyncService.configure(this.activeViewEnum);
}
ngDoCheck() {
if (this.valueSetter || this.selectionSetter) {
if (this.selection === 'range' &&
(this.value?.start || this.value?.end) &&
this.focusedDate.getTime() !== this.value.start?.getTime() &&
this.focusedDate.getTime() !== this.value.end?.getTime()) {
this.focusedDate = this.value.start || this.value.end || getToday();
}
this.setValue(this.value);
this.valueSetter = false;
this.selectionSetter = false;
}
if (hasExistingValue(this.changes, 'focusedDate')) {
const focusedDate = this.changes.focusedDate.currentValue;
this.focusedDate = dateInRange(focusedDate, this.min, this.max);
}
if (this.changes.navigation) {
this.syncNavigation = true;
}
if (this.changes.min || this.changes.max || this.changes.rangeValidation) {
this.minValidateFn = this.rangeValidation ? minValidator(this.min) : noop;
this.maxValidateFn = this.rangeValidation ? maxValidator(this.max) : noop;
this.onValidatorChange();
}
this.changes = {};
}
ngAfterViewInit() {
this.setAriaActivedescendant();
if (this.size !== 'none') {
const element = this.type === 'infinite' ? this.element : this.multiViewCalendar.element;
this.renderer.removeClass(element.nativeElement, getSizeClass('calendar', this.size));
this.renderer.addClass(element.nativeElement, getSizeClass('calendar', this.size));
}
}
ngAfterViewChecked() {
if (!this.syncNavigation) {
return;
}
this.syncNavigation = false;
this.scrollSyncService.sync(virtualizationProp(this.navigationView), virtualizationProp(this.monthView));
}
ngOnDestroy() {
this.scrollSyncService.destroy();
this.domEvents.forEach(unbindCallback => unbindCallback());
if (this.pickerService) {
this.pickerService.calendar = null;
}
if (this.viewChangeSubscription) {
this.viewChangeSubscription.unsubscribe();
}
if (this.pageChangeSubscription) {
this.pageChangeSubscription.unsubscribe();
}
if (this.localizationChangeSubscription) {
this.localizationChangeSubscription.unsubscribe();
}
this.destroyed = true;
}
/**
* @hidden
*/
onCellEnter(date) {
if (this.selection === 'range' && this.canHover) {
this.ngZone.run(() => {
if (this.allowReverse) {
if (this.activeRangeEnd === 'end' && this.selectionRange.start) {
this.selectionRange = { start: this.selectionRange.start, end: date };
}
if (this.activeRangeEnd === 'start' && this.selectionRange.end) {
this.selectionRange = { start: date, end: this.selectionRange.end };
}
}
else {
if (this.activeRangeEnd === 'end' && this.selectionRange.start && date >= this.selectionRange.start) {
this.selectionRange = { start: this.selectionRange.start, end: date };
}
if (this.selectionRange.start && date < this.selectionRange.start) {
this.selectionRange = { start: this.selectionRange.start, end: null };
}
}
});
}
}
/**
* @hidden
*/
onResize() {
this.focusedDate = new Date(this.focusedDate);
this.cdr.detectChanges();
}
/**
* Focuses the Calendar by making the table.k-calendar-table element active.
*/
focus() {
this.currentlyFocusedElement = this.type === 'infinite' ?
this.element?.nativeElement.querySelector(selectors.infiniteCalendarTable) :
this.currentlyFocusedElement = this.element?.nativeElement.querySelector(selectors.multiViewCalendarTable);
this.currentlyFocusedElement?.focus();
}
/**
* Blurs the Calendar component.
*/
blur() {
const blurTarget = this.type === 'infinite' ?
this.currentlyFocusedElement :
this.multiViewCalendar;
if (isPresent(blurTarget)) {
blurTarget.blur();
}
}
/**
* @hidden
*/
containsElement(element) {
return Boolean(closest(element, node => node === this.element.nativeElement));
}
/**
* @hidden
*/
handleNavigation(candidate) {
if (this.disabled) {
return;
}
const focusTarget = candidate ? new Date(cloneDate(candidate).setDate(1)) : this.focusedDate;
this.focusedDate = dateInRange(focusTarget, this.min, this.max);
this.detectChanges();
}
/**
* @hidden
*/
onPageChange() {
if (!NgZone.isInAngularZone()) {
if (this.pageChangeSubscription) {
this.pageChangeSubscription.unsubscribe();
}
this.pageChangeSubscription = fromPromise(this.resolvedPromise)
.subscribe(() => {
this.detectChanges(); // requires zone if templates
});
}
}
/**
* @hidden
*/
handleMultiViewCalendarValueChange(value, focusedDate) {
if (this.selection === 'range') {
this.valueChange.emit(value);
}
else {
const selectedDates = (Array.isArray(value) ? value : [value]);
this.handleDateChange({ selectedDates, focusedDate });
}
}
/**
* @hidden
*/
handleDateChange(args) {
const selectedDates = Array.isArray(args.selectedDates) ? args.selectedDates : [args.selectedDates];
const canNavigateDown = this.bus.canMoveDown(this.activeViewEnum);
const availableDates = selectedDates.filter(date => !this.disabledDatesService.isDateDisabled(date));
this.focusedDate = args.focusedDate || this.focusedDate;
if (this.disabled) {
return;
}
if (!canNavigateDown && areDatesEqual(availableDates, this.selectedDates)) {
this.emitSameDate();
return;
}
if (canNavigateDown) {
this.bus.moveDown(this.activeViewEnum);
return;
}
if (this.disabledDatesService.isDateDisabled(this.focusedDate)) {
return;
}
if (this.selection === 'range') {
return;
}
this.ngZone.run(() => {
this.selectedDates = availableDates.map(date => cloneDate(date));
this.value = this.parseSelectionToValue(availableDates);
this.onControlChange(this.parseSelectionToValue(availableDates));
this.valueChange.emit(this.parseSelectionToValue(availableDates));
this.cdr.markForCheck();
});
}
/**
* @hidden
*/
writeValue(candidate) {
this.verifyValue(candidate);
this.value = candidate;
this.cdr.markForCheck();
}
/**
* @hidden
*/
registerOnChange(fn) {
this.onControlChange = fn;
}
/**
* @hidden
*/
registerOnTouched(fn) {
this.onControlTouched = fn;
}
/**
* @hidden
*/
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this.cdr.markForCheck();
}
/**
* @hidden
*/
validate(control) {
return this.minValidateFn(control) || this.maxValidateFn(control);
}
/**
* @hidden
*/
registerOnValidatorChange(fn) {
this.onValidatorChange = fn;
}
/**
* @hidden
*/
activeCellTemplate() {
switch (this.activeViewEnum) {
case CalendarViewEnum.month:
return this.monthCellTemplateRef || this.cellTemplateRef;
case CalendarViewEnum.year:
return this.yearCellTemplateRef;
case CalendarViewEnum.decade:
return this.decadeCellTemplateRef;
case CalendarViewEnum.century:
return this.centuryCellTemplateRef;
default:
return null;
}
}
/**
* @hidden
*/
handleNavigate(event) {
this.focusedDate = event.focusedDate;
this.activeView = event.activeView;
this.emitNavigate(this.focusedDate);
}
/**
* @hidden
*/
emitNavigate(focusedDate) {
const activeView = CalendarViewEnum[this.activeViewEnum];
this.navigate.emit({ activeView, focusedDate });
}
/**
* @hidden
*/
emitEvent(emitter, args) {
if (hasObservers(emitter)) {
this.ngZone.run(() => {
emitter.emit(args);
});
}
}
/**
* @hidden
*/
handleActiveDateChange(date) {
this.activeViewDate = date;
this.emitEvent(this.activeViewDateChange, date);
}
/**
* @hidden
*/
handleActiveViewChange(view) {
this.activeView = view;
this.emitEvent(this.activeViewChange, view);
if (this.type === 'infinite') {
this.scrollSyncService.configure(this.activeViewEnum);
}
this.detectChanges(); // requires zone if templates
}
/**
* @hidden
*/
handleCellClick({ date, modifiers }) {
this.focus();
if (this.selection === 'range' && this.activeViewEnum === CalendarViewEnum[this.bottomView]) {
this.performRangeSelection(date);
}
else {
this.selectionService.lastClicked = date;
this.performSelection(date, modifiers);
}
}
/**
* @hidden
*/
handleWeekNumberClick(dates) {
if (this.selection === 'single') {
return;
}
this.ngZone.run(() => {
if (this.selection === 'multiple') {
this.handleDateChange({
selectedDates: dates,
focusedDate: last(dates),
});
}
if (this.selection === 'range') {
this.canHover = false;
this.activeRangeEnd = 'start';
const shouldEmitValueChange = this.selectionRange.start?.getTime() !== dates[0].getTime() ||
this.selectionRange.end?.getTime() !== last(dates).getTime();
this.value = { start: dates[0], end: last(dates) };
if (shouldEmitValueChange) {
this.valueChange.emit(this.value);
}
}
});
}
/**
* @hidden
*/
handleBlur(args) {
if (this.element.nativeElement.contains(args.relatedTarget)) {
return;
}
this.isActive = false;
// the injector can get the NgControl instance of the parent component (for example, the DateTimePicker)
// and enters the zone for no reason because the parent component is still untouched
if (!this.pickerService && requiresZoneOnBlur(this.control)) {
this.ngZone.run(() => {
this.onControlTouched();
this.emitBlur(args);
this.cdr.markForCheck();
});
}
else {
this.emitBlur(args);
this.detectChanges();
}
}
/**
* @hidden
*/
handleFocus() {
this.isActive = true;
if (!NgZone.isInAngularZone()) {
this.detectChanges();
}
this.emitFocus();
}
/**
* @hidden
*/
handleMultiViewCalendarKeydown(args) {
// Prevent form from submitting on enter if used in datepicker (classic view)
if (isPresent(this.pickerService) && args.keyCode === Keys.Enter) {
args.preventDefault();
}
}
setClasses(element) {
this.renderer.removeClass(element, `k-calendar-${this.type}`);
if (this.type === 'infinite') {
this.renderer.addClass(element, 'k-calendar');
this.renderer.addClass(element, `k-calendar-${this.type}`);
}
}
verifyChanges() {
if (!isDevMode()) {
return;
}
if (this.min > this.max) {
throw new Error(`The max value should be bigger than the min. See ${MIN_DOC_LINK} and ${MAX_DOC_LINK}.`);
}
if (this.bottomViewEnum > this.topViewEnum) {
throw new Error(`The topView should be greater than bottomView. See ${BOTTOM_VIEW_DOC_LINK} and ${TOP_VIEW_DOC_LINK}.`);
}
}
verifyValue(candidate) {
if (!isDevMode()) {
return;
}
if (this.selection === 'single' && candidate && !(isNullOrDate(candidate))) {
throw new Error(`When using 'single' selection the 'value' should be a valid JavaScript Date instance. Check ${VALUE_DOC_LINK} for possible resolution.`);
}
else if (this.selection === 'multiple' && candidate) {
if (Array.isArray(candidate)) {
const onlyDates = candidate.every(value => value instanceof Date);
if (!onlyDates) {
throw new Error(`When using 'multiple' selection the 'value' should be an array of valid JavaScript Date instances. Check ${VALUE_DOC_LINK} for possible resolution.`);
}
}
if (Object.keys(candidate).find(k => k === 'start') && Object.keys(candidate).find(k => k === 'end')) {
throw new Error(`When using 'multiple' selection the 'value' should be an array of valid JavaScript Date instances. Check ${VALUE_DOC_LINK} for possible resolution.`);
}
}
else if (this.selection === 'range' && candidate && !(isNullOrDate(candidate['start']) && isNullOrDate(candidate['end']))) {
throw new Error(`The 'value' should be an object with start and end dates. Check ${VALUE_DOC_LINK} for possible resolution.`);
}
}
bindEvents() {
const element = this.element.nativeElement;
this.domEvents.push(this.renderer.listen(element, 'focus', this.handleFocus.bind(this)), this.renderer.listen(element, 'mousedown', preventDefault), this.renderer.listen(element, 'click', this.handleComponentClick.bind(this)), this.renderer.listen(element, 'keydown', this.handleKeydown.bind(this)), this.renderer.listen(element, 'mouseleave', this.setRangeSelectionToValue.bind(this)));
}
setRangeSelectionToValue() {
if (this.selection === 'range' && this.type === 'infinite' && this.value) {
this.ngZone.run(() => {
this.selectionRange = this.value;
this.cdr.markForCheck();
});
}
}
emitBlur(args) {
if (this.pickerService) {
this.pickerService.onBlur.emit(args);
}
this.onBlur.emit();
}
emitFocus() {
if (this.pickerService) {
this.pickerService.onFocus.emit();
}
this.onFocus.emit();
}
handleComponentClick() {
if (!this.isActive) {
if (this.type === 'infinite' && this.monthView.isScrolled()) {
this.focusedDate = cloneDate(this.focusedDate); //XXX: forces change detect
this.detectChanges();
}
this.focus();
}
}
handleKeydown(args) {
if (this.type === 'infinite') {
// reserve the alt + arrow key commands for the picker
const ctrlKey = args.ctrlKey || args.metaKey;
const arrowKeyPressed = [Keys.ArrowUp, Keys.ArrowRight, Keys.ArrowDown, Keys.ArrowLeft].indexOf(args.keyCode) !== -1;
const reserveKeyCommandsForPicker = isPresent(this.pickerService) && arrowKeyPressed && args.altKey;
if (reserveKeyCommandsForPicker) {
return;
}
if (ctrlKey && arrowKeyPressed) {
args.preventDefault();
}
// Prevent form from submitting on enter if used in datepicker (infinite view)
const preventSubmitInDatePicker = isPresent(this.pickerService) && args.keyCode === Keys.Enter;
if (preventSubmitInDatePicker) {
args.preventDefault();
}
const candidate = dateInRange(this.navigator.move(this.focusedDate, this.navigator.action(args), this.activeViewEnum), this.min, this.max);
if (!isEqual(this.focusedDate, candidate)) {
this.focusedDate = candidate;
this.detectChanges();
args.preventDefault();
}
if (args.keyCode === Keys.Enter) {
this.selectionService.lastClicked = this.focusedDate;
if (this.selection !== 'range') {
this.performSelection(this.focusedDate, args);
}
else {
this.performRangeSelection(this.focusedDate);
}
}
if (args.keyCode === Keys.KeyT) {
this.focusToday();
}
if (isArrowWithShiftPressed(args) && this.selection !== 'range') {
args.anyArrow = true;
this.performSelection(this.focusedDate, args);
}
}
}
focusToday() {
this.focusedDate = getToday();
this.bus.moveToBottom(this.activeViewEnum);
this.cdr.detectChanges();
}
detectChanges() {
if (!this.destroyed) {
this.cdr.detectChanges();
}
}
emitSameDate() {
if (this.pickerService) {
this.pickerService.sameDateSelected.emit();
}
}
setAriaActivedescendant() {
// in Classic mode, the inner MultiViewCalendar handles the activedescendant
const infiniteCalendarTable = this.element.nativeElement?.querySelector(selectors.infiniteCalendarTable);
const activedescendantHandledByInnerMultiViewCalendar = !isPresent(infiniteCalendarTable) || (this.type === 'classic' && !infiniteCalendarTable.hasAttribute(attributeNames.ariaActiveDescendant));
if (activedescendantHandledByInnerMultiViewCalendar) {
return;
}
if (this.type === 'classic') {
this.renderer.removeAttribute(infiniteCalendarTable, attributeNames.ariaActiveDescendant);
return;
}
const focusedCellId = this.cellUID + this.focusedDate.getTime();
this.renderer.setAttribute(infiniteCalendarTable, attributeNames.ariaActiveDescendant, focusedCellId);
}
parseSelectionToValue(selection) {
selection = selection || [];
return this.selection === 'single' ? cloneDate(last(selection)) : selection.map(date => cloneDate(date));
}
setValue(candidate) {
this.verifyValue(candidate);
if (candidate === null) {
this._value = null;
this.selectedDates = [];
this.selectionRange = { start: null, end: null };
}
else if (Array.isArray(candidate)) {
this.selectionRange = { start: null, end: null };
this._value = candidate.filter(date => isPresent(date)).map(element => cloneDate(element));
}
else if (isObject(candidate) && Object.keys(candidate).find(k => k === 'start') && Object.keys(candidate).find(k => k === 'end')) {
this.selectedDates = [];
this.selectionRange = { start: null, end: null };
this._value = { start: null, end: null };
this._value.start = candidate.start instanceof Date ? cloneDate(candidate.start) : null;
this._value.end = candidate.end instanceof Date ? cloneDate(candidate.end) : null;
this.selectionRange = Object.assign({}, this._value);
if (this._value?.start && !this._value?.end) {
this.activeRangeEnd = 'end';
this.canHover = true;
}
if (this._value?.end && !this._value?.start) {
this.activeRangeEnd = 'start';
this.canHover = true;
}
if (this._value?.end && this._value?.start) {
this.canHover = false;
}
}
else {
this.selectionRange = { start: null, end: null };
this._value = cloneDate(candidate);
}
if (this.selection !== 'range') {
const selection = [].concat(candidate).filter(date => isPresent(date)).map(date => cloneDate(date));
if (!areDatesEqual(selection, this.selectedDates)) {
const lastSelected = last(selection);
this.rangePivot = cloneDate(lastSelected);
this.focusedDate = cloneDate(lastSelected) || this.focusedDate;
this.selectedDates = selection;
}
}
}
performRangeSelection(date) {
this.focusedDate = date;
const clonedRangeSelection = Object.assign({}, this.selectionRange);
const emitValueChange = (this.activeRangeEnd === 'start' && this.value?.start?.getTime() !== date?.getTime()) ||
(this.activeRangeEnd === 'end' && this.value?.end?.getTime() !== date?.getTime());
this.ngZone.run(() => {
const rangeSelection = handleRangeSelection(date, clonedRangeSelection, this.activeRangeEnd, this.allowReverse);
this.activeRangeEnd = rangeSelection.activeRangeEnd;
if (this.canHover && rangeSelection.activeRangeEnd === 'end' && rangeSelection.selectionRange.end?.getTime() === date.getTime()) {
this.activeRangeEnd = 'start';
rangeSelection.activeRangeEnd = 'start';
}
this.canHover = this.activeRangeEnd === 'end' && rangeSelection.selectionRange.start && !rangeSelection.selectionRange.end;
if (emitValueChange && (this.value?.start?.getTime() !== rangeSelection.selectionRange?.start?.getTime() ||
this.value?.end?.getTime() !== rangeSelection.selectionRange?.end?.getTime())) {
this.value = rangeSelection.selectionRange;
this.valueChange.emit(this.value);
}
this.cdr.markForCheck();
});
}
performSelection(date, selectionModifiers) {
const selection = this.selectionService.performSelection({
date: date,
modifiers: selectionModifiers,
selectionMode: this.selection,
activeViewEnum: this.activeViewEnum,
rangePivot: this.rangePivot,
selectedDates: this.selectedDates
});
this.rangePivot = selection.rangePivot;
this.handleDateChange({
selectedDates: selection.selectedDates,
focusedDate: date
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CalendarComponent, deps: [{ token: i1.BusViewService }, { token: i2.CalendarDOMService }, { token: i0.ElementRef }, { token: i3.NavigationService }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i0.Injector }, { token: i4.ScrollSyncService }, { token: i5.DisabledDatesService }, { token: i6.LocalizationService }, { token: i7.SelectionService }, { token: i8.PickerService, optional: true }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: CalendarComponent, isStandalone: true, selector: "kendo-calendar", inputs: { showOtherMonthDays: "showOtherMonthDays", id: "id", focusedDate: "focusedDate", min: "min", max: "max", rangeValidation: "rangeValidation", weekDaysFormat: "weekDaysFormat", footer: "footer", selection: "selection", allowReverse: "allowReverse", value: "value", disabled: "disabled", tabindex: "tabindex", tabIndex: "tabIndex", disabledDates: "disabledDates", navigation: "navigation", activeView: "activeView", bottomView: "bottomView", topView: "topView", type: "type", animateNavigation: "animateNavigation", weekNumber: "weekNumber", cellTemplateRef: ["cellTemplate", "cellTemplateRef"], monthCellTemplateRef: ["monthCellTemplate", "monthCellTemplateRef"], yearCellTemplateRef: ["yearCellTemplate", "yearCellTemplateRef"], decadeCellTemplateRef: ["decadeCellTemplate", "decadeCellTemplateRef"], centuryCellTemplateRef: ["centuryCellTemplate", "centuryCellTemplateRef"], weekNumberTemplateRef: ["weekNumberTemplate", "weekNumberTemplateRef"], headerTitleTemplateRef: ["headerTitleTemplate", "headerTitleTemplateRef"], headerTemplateRef: ["headerTemplate", "headerTemplateRef"], footerTemplateRef: ["footerTemplate", "footerTemplateRef"], navigationItemTemplateRef: ["navigationItemTemplate", "navigationItemTemplateRef"], size: "size", activeRangeEnd: "activeRangeEnd" }, outputs: { closePopup: "closePopup", activeViewChange: "activeViewChange", navigate: "navigate", activeViewDateChange: "activeViewDateChange", onBlur: "blur", onFocus: "focus", valueChange: "valueChange" }, host: { properties: { "class.k-week-number": "this.weekNumber", "attr.id": "this.widgetId", "attr.aria-disabled": "this.ariaDisabled", "class.k-disabled": "this.ariaDisabled" } }, providers: [
BusViewService,
CALENDAR_VALUE_ACCESSOR,
CALENDAR_RANGE_VALIDATORS,
KENDO_INPUT_PROVIDER,
LocalizationService,
DisabledDatesService,
{
provide: L10N_PREFIX,
useValue: 'kendo.calendar'
},
NavigationService,
ScrollSyncService,
SelectionService
], queries: [{ propertyName: "cellTemplate", first: true, predicate: CellTemplateDirective, descendants: true }, { propertyName: "monthCellTemplate", first: true, predicate: MonthCellTemplateDirective, descendants: true }, { propertyName: "yearCellTemplate", first: true, predicate: YearCellTemplateDirective, descendants: true }, { property