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,297 lines 70.9 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, 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