@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,272 lines (1,268 loc) • 62 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, HostListener, Input, Output, ViewChild, NgZone } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n';
import { cloneDate, isEqual } from '@progress/kendo-date-math';
import { hasObservers, guid, Keys, isObject } from '@progress/kendo-angular-common';
import { HorizontalViewListComponent } from './horizontal-view-list.component';
import { HeaderComponent } from './header.component';
import { BusViewService } from './services/bus-view.service';
import { SelectionService } from './services/selection.service';
import { DisabledDatesService } from './services/disabled-dates.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 { Action } from './models/navigation-action.enum';
import { CalendarViewEnum } from './models/view.enum';
import { handleRangeSelection } from './models/selection';
import { minValidator } from '../validators/min.validator';
import { maxValidator } from '../validators/max.validator';
import { disabledDatesRangeValidator } from '../validators/disabled-dates-range.validator';
import { MIN_DATE, MAX_DATE } from '../defaults';
import { DEFAULT_SIZE, areDatesEqual, dateInRange, getSizeClass, getToday, hasExistingValue, last, noop } from '../util';
import { Subscription } from 'rxjs';
import { isArrowWithShiftPressed, isNullOrDate, isPresent } from '../common/utils';
import { NavigationService } from './services/navigation.service';
import { HeaderTemplateDirective } from './templates/header-template.directive';
import { FooterTemplateDirective } from './templates/footer-template.directive';
import { FooterComponent } from './footer.component';
import { NgIf } from '@angular/common';
import { MultiViewCalendarLocalizedMessagesDirective } from './localization/multiview-calendar-localized-messages.directive';
import * as i0 from "@angular/core";
import * as i1 from "./services/bus-view.service";
import * as i2 from "./services/navigation.service";
import * as i3 from "./services/disabled-dates.service";
import * as i4 from "./services/selection.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';
/**
* @hidden
*/
export const RANGE_CALENDAR_VALUE_ACCESSOR = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MultiViewCalendarComponent)
};
/**
* @hidden
*/
export const RANGE_CALENDAR_RANGE_VALIDATORS = {
multi: true,
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MultiViewCalendarComponent)
};
/**
* Represents the Kendo UI MultiViewCalendar component for Angular.
*
* @example
* ```ts
* _@Component({
* selector: 'my-app',
* template: `
* <kendo-multiviewcalendar></kendo-multiviewcalendar>
* `
* })
* export class AppComponent { }
* ```
*/
export class MultiViewCalendarComponent {
bus;
element;
navigator;
renderer;
cdr;
zone;
disabledDatesService;
selectionService;
/**
* Displays the days that fall out of the current month ([see example]({% slug viewoptions_multiviewcalendar %}#toc-displaying-other-month-days)).
* @default true
*/
showOtherMonthDays = true;
/**
* @hidden
*
* Determines whether to display the calendar header.
*/
showCalendarHeader = true;
/**
* @hidden
*
* TODO: Make visible when the Infinite Calendar is fixed to set properly the size option.
* Sets the size of the component.
*
* The possible values are:
* * `small`
* * `medium` (Default)
* * `large`
* * `none`
*
*/
set size(size) {
const newSize = size ? size : DEFAULT_SIZE;
this.renderer.removeClass(this.element.nativeElement, getSizeClass('calendar', this.size));
if (newSize !== 'none') {
this.renderer.addClass(this.element.nativeElement, getSizeClass('calendar', newSize));
}
this._size = newSize;
}
get size() {
return this._size;
}
_size = DEFAULT_SIZE;
/**
* @hidden
*/
id;
/**
* Sets or gets the `focusedDate` property of the Calendar and
* defines the focused date of the component
* ([see example]({% slug dates_multiviewcalendar %}#toc-focused-dates)).
*
* > If the Calendar is out of the min or max range, it normalizes the defined `focusedDate`.
*/
set focusedDate(focusedDate) {
this._focusedDate = focusedDate || getToday();
}
get focusedDate() {
return this._focusedDate;
}
/**
* Toggles the visibility of the footer.
* @default false
*/
footer = false;
/**
* @hidden
*/
get headerId() {
return this.id + 'header-';
}
/**
* @hidden
*/
get multiViewCalendarHeaderIdLabel() {
return this.views >= 2 ? this.id + 'header-' : null;
}
/**
* @hidden
*/
get calendarHeaderIdLabel() {
return this.views === 1 ? this.id + 'header-' : null;
}
/**
* Sets or gets the `min` property of the Calendar and
* defines the minimum allowed date value.
* By default, the `min` value is `1900-1-1`.
*/
set min(min) {
this._min = min || new Date(MIN_DATE);
}
get min() {
return this._min;
}
/**
* Sets or gets the `max` property of the Calendar and
* defines the maximum allowed date value.
* By default, the `max` value is `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.
*/
rangeValidation = false;
/**
* Determines whether the built-in validator for disabled
* date ranges is enforced when validating a form
* ([see example](slug:disabled_dates_multiviewcalendar)).
*/
disabledDatesRangeValidation = false;
/**
* Sets the Calendar selection mode
* ([see example]({% slug selection_multiviewcalendar %})).
*
* The available values are:
* * `single` (default)
* * `multiple`
* * `range`
*/
set selection(_selection) {
this.selectionSetter = true;
this._selection = _selection;
}
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_multiviewcalendar %})).
* To learn how to disable the component in reactive forms, refer to the article on [Forms Support](slug:formssupport_multiviewcalendar#toc-managing-the-multiviewcalendar-disabled-state-in-reactive-forms).
*/
disabled = false;
/**
* Sets or gets 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.
*/
tabindex = 0;
/**
* @hidden
*/
set tabIndex(tabIndex) {
this.tabindex = tabIndex;
}
get tabIndex() {
return this.tabindex;
}
/**
* Sets the format of the displayed Calendar week days' names.
* @default 'short'
*/
weekDaysFormat = "short";
/**
* @hidden
*/
isActive = false;
/**
* Sets the dates of the MultiViewCalendar that will be disabled
* ([see example]({% slug disabled_dates_multiviewcalendar %})).
*/
set disabledDates(value) {
this.disabledDatesService.initialize(value);
}
/**
* Defines the active view that the Calendar initially renders
* ([see example]({% slug viewoptions_multiviewcalendar %})).
* By default, the active view is `month`.
*
* > You have to set `activeView` within the `topView`-`bottomView` range.
*/
activeView = CalendarViewEnum[CalendarViewEnum.month];
/**
* Defines the bottommost view, to which the user can navigate
* ([see example](slug:viewdepth_multiviewcalendar)).
*/
bottomView = CalendarViewEnum[CalendarViewEnum.month];
/**
* Defines the topmost view, to which the user can navigate.
*/
topView = CalendarViewEnum[CalendarViewEnum.century];
/**
* Determines whether to display a header for every view (for example the month name).
*/
showViewHeader = false;
/**
* Determines whether to enable animation when navigating to previous/next view.
*
* > 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_multiviewcalendar %})).
*/
weekNumber = false;
/**
* Specify, 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'.
*/
set activeRangeEnd(_activeRangeEnd) {
this._activeRangeEnd = _activeRangeEnd;
}
get activeRangeEnd() {
return (this.selection === 'range' && !this._activeRangeEnd) ? 'start' : this._activeRangeEnd;
}
/**
* Sets or gets the `selectionRange` property of the Calendar and
* defines the selection range of the component
* ([see example](slug:selection_multiviewcalendar#toc-range-selection)).
* > We recommend using the `value` property as it now supports `range` selection.
*/
set selectionRange(range) {
this._selectionRange = range;
if (this.disabledDatesRangeValidation) {
this.onValidatorChange();
}
}
get selectionRange() {
return this._selectionRange;
}
/**
* Sets or gets the `views` property of the Calendar and
* defines the number of rendered months.
*/
views = 2;
/**
* Specifies the orientation of the MultiViewCalendar.
*
* The available values are:
* * `horizontal` (default)
* * `vertical`
*/
orientation = 'horizontal';
/**
* Fires when the active view is changed
* ([see example](slug:events_multiviewcalendar)).
*/
activeViewChange = new EventEmitter();
/**
* Fires when navigating in the currently active view
* ([see example](slug:events_multiviewcalendar)).
*/
navigate = new EventEmitter();
/**
* Fires when a view cell is entered
* ([see example](slug:events_multiviewcalendar)).
*/
cellEnter = new EventEmitter();
/**
* Fires when a view cell is leaved
* ([see example](slug:events_multiviewcalendar)).
*/
cellLeave = new EventEmitter();
/**
* Fires when the value is changed
* ([see example](slug:events_multiviewcalendar)).
*/
valueChange = new EventEmitter();
/**
* @hidden
* Fires when the range selection changes.
*/
rangeSelectionChange = new EventEmitter();
/**
* Fires each time the MultiViewCalendar gets blurred
* ([see example](slug:events_multiviewcalendar)).
*/
blurEvent = new EventEmitter();
/**
* Fires each time the MultiViewCalendar gets focused
* ([see example](slug:events_multiviewcalendar)).
*/
focusEvent = new EventEmitter();
/**
* @hidden
*/
focusCalendar = new EventEmitter();
/**
* @hidden
*/
onClosePopup = new EventEmitter();
/**
* @hidden
*/
onTabPress = new EventEmitter();
/**
* @hidden
*/
onShiftTabPress = 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 KendoMultiViewCalendar 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 KendoMultiViewCalendar 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 KendoMultiViewCalendar 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 KendoMultiViewCalendar 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 KendoMultiViewCalendar 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 KendoMultiViewCalendar 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.
*/
headerTemplate;
/**
* @hidden
*
* Queries the template for a header template declaration.
*/
footerTemplate;
/**
* @hidden
*
* Defines the template for the Calendar footer.
* Takes precedence over nested templates in the KendoMultiViewCalendar tag.
*/
set footerTemplateRef(template) {
this._footerTemplateRef = template;
}
get footerTemplateRef() {
return this._footerTemplateRef || this.footerTemplate;
}
/**
* @hidden
*
* Defines the template for the header title.
* Takes precedence over nested templates in the KendoMultiViewCalendar tag.
*/
set headerTitleTemplateRef(template) {
this._headerTitleTemplateRef = template;
}
get headerTitleTemplateRef() {
return this._headerTitleTemplateRef || this.headerTitleTemplate;
}
/**
* @hidden
*
* Defines the template for the Calendar header.
* Takes precedence over nested templates in the KendoMultiViewCalendar tag.
*/
set headerTemplateRef(template) {
this._headerTemplateRef = template;
}
get headerTemplateRef() {
return this._headerTemplateRef || this.headerTemplate;
}
headerElement;
viewList;
cellUID = guid();
isHovered = false;
activeDate;
isPrevDisabled = true;
isNextDisabled = true;
prevView = Action.PrevView;
nextView = Action.NextView;
selectedDates = [];
rangePivot;
shouldHoverWhenNoStart = false;
canHover = false;
changes = {};
valueSetter = false;
selectionSetter = false;
_min = new Date(MIN_DATE);
_max = new Date(MAX_DATE);
_focusedDate = getToday();
_value;
_selectionRange = { start: null, end: null };
_activeRangeEnd;
resolvedPromise = Promise.resolve();
onControlChange = noop;
onControlTouched = noop;
onValidatorChange = noop;
minValidateFn = noop;
maxValidateFn = noop;
disabledDatesRangeValidateFn = noop;
subscriptions = new Subscription();
_cellTemplateRef;
_monthCellTemplateRef;
_yearCellTemplateRef;
_decadeCellTemplateRef;
_centuryCellTemplateRef;
_weekNumberTemplateRef;
_headerTitleTemplateRef;
_headerTemplateRef;
_footerTemplateRef;
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.views >= 2 ? this.id : null;
}
get ariaDisabled() {
return this.disabled;
}
/**
* @hidden
*/
get ariaActivedescendant() {
return this.cellUID + this.focusedDate.getTime();
}
/**
* @hidden
*/
handleFocusout(event) {
const relatedTarget = event.relatedTarget;
if (!this.element.nativeElement.contains(relatedTarget)) {
const isClassicCalendar = this.views === 1;
isClassicCalendar ? this.blurEvent.emit(event) : this.blurEvent.emit();
this.onControlTouched();
}
this.isActive = false;
this.isHovered = false; //ensure that hovered is also not active
}
/**
* @hidden
*/
handleFocus() {
this.isActive = true;
const isClassicCalendar = this.views === 1;
isClassicCalendar ? this.focusCalendar.emit() : this.focusEvent.emit();
this.focusEvent.emit();
}
/**
* @hidden
*/
handleMouseEnter() {
this.isHovered = true;
}
/**
* @hidden
*/
handleMouseLeave() {
this.isHovered = false;
this.setRangeSelectionToValue();
}
/**
* @hidden
*/
handleMousedown(event) {
event.preventDefault();
}
/**
* @hidden
*/
handleClick() {
if (this.isActive) {
return;
}
this.focus();
}
/**
* @hidden
*/
keydown(event) {
const arrowUpOrDownKeyPressed = [Keys.ArrowUp, Keys.ArrowDown].indexOf(event.keyCode) !== -1;
const ctrlKey = event.ctrlKey || event.metaKey;
const onArrowRightAndControl = event.keyCode === Keys.ArrowRight && ctrlKey;
const onArrowLeftAndControl = event.keyCode === Keys.ArrowLeft && ctrlKey;
const onTKeyPress = event.keyCode === Keys.KeyT;
const onEnterKeyPress = event.keyCode === Keys.Enter;
const onArrowUpPress = event.keyCode === Keys.ArrowUp;
const altKey = event.altKey;
const escKey = event.keyCode === Keys.Escape;
const tabKeyPress = event.keyCode === Keys.Tab;
const shiftKeyPress = event.shiftKey;
if (onArrowRightAndControl) {
event.preventDefault();
this.navigateView(this.nextView);
return;
}
else if (onArrowLeftAndControl) {
event.preventDefault();
this.navigateView(this.prevView);
return;
}
else if (ctrlKey && arrowUpOrDownKeyPressed) {
event.preventDefault();
}
else if (onTKeyPress) {
this.focusedDate = getToday();
this.bus.moveToBottom(this.activeViewEnum);
this.updateButtonState();
return;
}
else if (onEnterKeyPress) {
if (this.selection !== 'range') {
this.selectionService.lastClicked = this.focusedDate;
this.performSelection(this.focusedDate, event);
}
else {
this.performRangeSelection(this.focusedDate);
}
}
if (this.views >= 2) {
if ((escKey || (altKey && onArrowUpPress))) {
this.onClosePopup.emit(event);
}
else if ((tabKeyPress && shiftKeyPress)) {
this.onShiftTabPress.emit(event);
}
else if ((tabKeyPress && !shiftKeyPress)) {
this.onTabPress.emit(event);
}
}
const candidate = dateInRange(this.navigator.move(this.focusedDate, this.navigator.action(event), this.activeViewEnum), this.min, this.max);
if (isEqual(this.focusedDate, candidate)) {
return;
}
this.focusedDate = candidate;
event.preventDefault();
const isSameView = this.bus.service(this.activeViewEnum).isInArray(this.focusedDate, this.viewList.dates);
if (!isSameView) {
this.emitNavigate(this.focusedDate);
this.updateButtonState();
}
if (isArrowWithShiftPressed(event) && this.selection !== 'range') {
event['anyArrow'] = true;
this.performSelection(this.focusedDate, event);
}
}
constructor(bus, element, navigator, renderer, cdr, zone, disabledDatesService, selectionService) {
this.bus = bus;
this.element = element;
this.navigator = navigator;
this.renderer = renderer;
this.cdr = cdr;
this.zone = zone;
this.disabledDatesService = disabledDatesService;
this.selectionService = selectionService;
this.id = `kendo-multiviewcalendarid-${this.bus.calendarId}-`;
}
ngOnInit() {
this.setClasses(this.element.nativeElement);
this.subscriptions.add(this.bus.viewChanged.subscribe(({ view }) => {
this.activeView = CalendarViewEnum[view];
this.activeViewChange.emit(this.activeView);
this.cdr.detectChanges();
this.updateButtonState();
}));
}
ngOnChanges(changes) {
this.changes = changes;
this.verifyChanges();
this.bus.configure(this.bottomViewEnum, this.topViewEnum);
}
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.min || this.changes.max || this.changes.rangeValidation || this.changes.disabledDates || this.changes.disabledDatesRangeValidation) {
this.minValidateFn = this.rangeValidation ? minValidator(this.min) : noop;
this.maxValidateFn = this.rangeValidation ? maxValidator(this.max) : noop;
this.disabledDatesRangeValidateFn = this.disabledDatesRangeValidation ? disabledDatesRangeValidator(this.disabledDatesService.isDateDisabled) : noop;
this.onValidatorChange();
}
if (this.changes.min || this.changes.max || this.changes.focusedDate || this.changes.activeView || this.changes.value) {
this.updateButtonState();
}
this.changes = {};
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
ngAfterViewInit() {
this.updateButtonState();
}
/**
* Focuses the host element of the Calendar.
*
* @example
* ```ts
* _@Component({
* selector: 'my-app',
* template: `
* <button (click)="multiviewcalendar.focus()">Focus calendar</button>
* <kendo-multiviewcalendar #multiviewcalendar></kendo-multiviewcalendar>
* `
* })
* export class AppComponent { }
* ```
*/
focus() {
if (!this.element) {
return;
}
// Prevent the content from auto-scrolling when daterange is in adaptive mode
this.element.nativeElement.querySelector('.k-calendar-view').focus({ preventScroll: true });
}
/**
* Blurs the Calendar component.
*/
blur() {
if (!this.element) {
return;
}
const activeElement = this.views >= 2 ? this.element.nativeElement.querySelector('.k-calendar-view') :
this.element.nativeElement.querySelector('.k-calendar-table');
activeElement.blur();
}
/**
* @hidden
*/
handleDateChange(args) {
const canNavigateDown = this.bus.canMoveDown(this.activeViewEnum);
const availableDates = args.selectedDates.filter(date => !this.disabledDatesService.isDateDisabled(date));
this.focusedDate = args.focusedDate || this.focusedDate;
const sameDates = !canNavigateDown && areDatesEqual(availableDates, this.selectedDates);
if (this.disabled || sameDates) {
return;
}
if (canNavigateDown) {
this.bus.moveDown(this.activeViewEnum);
return;
}
if (this.disabledDatesService.isDateDisabled(this.focusedDate)) {
return;
}
if (this.selection === 'range') {
return;
}
this.selectedDates = availableDates.map(date => cloneDate(date));
this.value = this.parseSelectionToValue(availableDates);
this.onControlChange(this.parseSelectionToValue(availableDates));
this.valueChange.emit(this.parseSelectionToValue(availableDates));
}
/**
* @hidden
*/
onCellEnter(cellEnter, date) {
this.emitCellEvent(cellEnter, date);
if (this.selection === 'range' && (this.canHover || this.shouldHoverWhenNoStart)) {
this.zone.run(() => {
if (this.canHover && !this.shouldHoverWhenNoStart) {
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 };
}
}
}
else if (this.shouldHoverWhenNoStart && date <= this.selectionRange.end) {
this.selectionRange = { start: date, end: this.selectionRange.end };
}
else {
this.selectionRange = { start: null, end: this.selectionRange.end };
}
});
}
}
/**
* @hidden
*/
handleTodayButtonClick(args) {
const todayDate = args.focusedDate;
const isSameView = this.bus.service(this.activeViewEnum).isInArray(todayDate, this.viewList.dates);
const isBottomView = !this.bus.canMoveDown(this.activeViewEnum);
if (!isSameView && isBottomView) {
this.emitNavigate(todayDate);
this.updateButtonState();
}
this.handleDateChange(args);
}
/**
* @hidden
*/
setActiveDate(date) {
this.activeDate = cloneDate(date);
this.cdr.detectChanges();
}
/**
* @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) || this.disabledDatesRangeValidateFn(this.selectionRange);
}
/**
* @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
*/
navigateView(action) {
this.focusedDate = this.viewList.navigate(action);
this.updateButtonState();
this.emitNavigate(this.focusedDate);
}
/**
* @hidden
*/
emitNavigate(focusedDate) {
const activeView = CalendarViewEnum[this.activeViewEnum];
this.navigate.emit({ activeView, focusedDate });
}
/**
* @hidden
*/
emitCellEvent(emitter, args) {
if (hasObservers(emitter)) {
this.zone.run(() => {
emitter.emit(args);
});
}
}
/**
* @hidden
*/
handleCellClick({ date, modifiers }) {
if (this.selection === 'range' && this.activeViewEnum === CalendarViewEnum[this.bottomView]) {
this.performRangeSelection(date);
}
else {
this.selectionService.lastClicked = date;
this.performSelection(date, modifiers);
}
const isSameView = this.bus.service(this.activeViewEnum).isInArray(this.focusedDate, this.viewList.dates);
if (!isSameView) {
this.emitNavigate(this.focusedDate);
this.updateButtonState();
}
}
/**
* @hidden
*/
handleWeekNumberClick(dates) {
if (this.selection === 'single') {
return;
}
this.zone.run(() => {
if (this.selection === 'multiple') {
this.handleDateChange({
selectedDates: dates,
focusedDate: last(dates),
});
}
if (this.selection === 'range') {
this.activeRangeEnd = 'start';
const shouldEmitValueChange = this.selectionRange.start?.getTime() !== dates[0].getTime() ||
this.selectionRange.end?.getTime() !== last(dates).getTime();
this.selectionRange.start = dates[0];
this.selectionRange.end = last(dates);
this.value = this.selectionRange;
if (shouldEmitValueChange) {
this.valueChange.emit(this.value);
}
}
});
}
setClasses(element) {
this.renderer.addClass(element, 'k-calendar');
this.renderer.addClass(element, getSizeClass('calendar', this.size));
if (this.views >= 2) {
this.renderer.addClass(element, 'k-calendar-range');
}
}
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.`);
}
}
updateButtonState() {
this.resolvedPromise.then(() => {
this.cdr.detectChanges();
this.isPrevDisabled = !this.viewList.canNavigate(this.prevView);
this.isNextDisabled = !this.viewList.canNavigate(this.nextView);
this.cdr.markForCheck();
});
}
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 = [];
}
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;
}
}
}
setRangeSelectionToValue() {
if (this.selection === 'range' && this.value) {
this.selectionRange = this.value;
this.cdr.markForCheck();
}
}
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.zone.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.rangeSelectionChange.emit(rangeSelection);
}
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: MultiViewCalendarComponent, deps: [{ token: i1.BusViewService }, { token: i0.ElementRef }, { token: i2.NavigationService }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i3.DisabledDatesService }, { token: i4.SelectionService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MultiViewCalendarComponent, isStandalone: true, selector: "kendo-multiviewcalendar", inputs: { showOtherMonthDays: "showOtherMonthDays", showCalendarHeader: "showCalendarHeader", size: "size", id: "id", focusedDate: "focusedDate", footer: "footer", min: "min", max: "max", rangeValidation: "rangeValidation", disabledDatesRangeValidation: "disabledDatesRangeValidation", selection: "selection", allowReverse: "allowReverse", value: "value", disabled: "disabled", tabindex: "tabindex", tabIndex: "tabIndex", weekDaysFormat: "weekDaysFormat", isActive: "isActive", disabledDates: "disabledDates", activeView: "activeView", bottomView: "bottomView", topView: "topView", showViewHeader: "showViewHeader", animateNavigation: "animateNavigation", weekNumber: "weekNumber", activeRangeEnd: "activeRangeEnd", selectionRange: "selectionRange", views: "views", orientation: "orientation", cellTemplateRef: ["cellTemplate", "cellTemplateRef"], monthCellTemplateRef: ["monthCellTemplate", "monthCellTemplateRef"], yearCellTemplateRef: ["yearCellTemplate", "yearCellTemplateRef"], decadeCellTemplateRef: ["decadeCellTemplate", "decadeCellTemplateRef"], centuryCellTemplateRef: ["centuryCellTemplate", "centuryCellTemplateRef"], weekNumberTemplateRef: ["weekNumberTemplate", "weekNumberTemplateRef"], footerTemplateRef: ["footerTemplate", "footerTemplateRef"], headerTitleTemplateRef: ["headerTitleTemplate", "headerTitleTemplateRef"], headerTemplateRef: ["headerTemplate", "headerTemplateRef"] }, outputs: { activeViewChange: "activeViewChange", navigate: "navigate", cellEnter: "cellEnter", cellLeave: "cellLeave", valueChange: "valueChange", rangeSelectionChange: "rangeSelectionChange", blurEvent: "blur", focusEvent: "focus", focusCalendar: "focusCalendar", onClosePopup: "onClosePopup", onTabPress: "onTabPress", onShiftTabPress: "onShiftTabPress" }, host: { listeners: { "mouseenter": "handleMouseEnter()", "mouseleave": "handleMouseLeave()", "mousedown": "handleMousedown($event)", "click": "handleClick()", "keydown": "keydown($event)" }, properties: { "attr.id": "this.widgetId", "attr.aria-disabled": "this.ariaDisabled", "class.k-disabled": "this.ariaDisabled" } }, providers: [
BusViewService,
RANGE_CALENDAR_VALUE_ACCESSOR,
RANGE_CALENDAR_RANGE_VALIDATORS,
LocalizationService,
DisabledDatesService,
{
provide: L10N_PREFIX,
useValue: 'kendo.multiviewcalendar'
},
NavigationService,
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 }, { propertyName: "decadeCellTemplate", first: true, predicate: DecadeCellTemplateDirective, descendants: true }, { propertyName: "centuryCellTemplate", first: true, predicate: CenturyCellTemplateDirective, descendants: true }, { propertyName: "weekNumberTemplate", first: true, predicate: WeekNumberCellTemplateDirective, descendants: true }, { propertyName: "headerTitleTemplate", first: true, predicate: HeaderTitleTemplateDirective, descendants: true }, { propertyName: "headerTemplate", first: true, predicate: HeaderTemplateDirective, descendants: true }, { propertyName: "footerTemplate", first: true, predicate: FooterTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "headerElement", first: true, predicate: HeaderComponent, descendants: true, read: ElementRef }, { propertyName: "viewList", first: true, predicate: HorizontalViewListComponent, descendants: true }], exportAs: ["kendo-multiviewcalendar"], usesOnChanges: true, ngImport: i0, template: `
<ng-container kendoMultiViewCalendarLocalizedMessages
i18n-today="kendo.multiviewcalendar.today|The label for the today button in the calendar header"
today="Today"
i18n-prevButtonTitle="kendo.multiviewcalendar.prevButtonTitle|The label for the previous button in the Multiview calendar"
prevButtonTitle="Navigate to previous view"
i18n-nextButtonTitle="kendo.multiviewcalendar.nextButtonTitle|The label for the next button in the Multiview calendar"
nextButtonTitle="Navigate to next view"
i18n-parentViewButtonTitle="kendo.multiviewcalendar.parentViewButtonTitle|The title of the parent view button in the Multiview calendar header"
parentViewButtonTitle="Navigate to parent view"
>
</ng-container>
<kendo-calendar-header
*ngIf="showCalendarHeader"
[activeView]="activeViewEnum"
[currentDate]="activeDate"
[size]="size"
[min]="min"
[max]="max"
[id]="headerId"
[rangeLength]="views"
[titleTemplateRef]="headerTitleTemplateRef?.templateRef"
[headerTemplateRef]="headerTemplateRef?.templateRef"
[isPrevDisabled]="isPrevDisabled"
[isNextDisabled]="isNextDisabled"
[showNavigationButtons]="true"
[orientation]="orientation"
(todayButtonClick)="handleTodayButtonClick({ selectedDates: [$event], focusedDate: $event })"
(prevButtonClick)="navigateView(prevView)"
(nextButtonClick)="navigateView(nextView)"
>
</kendo-calendar-header>
<kendo-calendar-horizontal
[showOtherMonthDays]="showOtherMonthDays"
[allowReverse]="allowReverse"
[id]="calendarHeaderIdLabel"
[attr.aria-labelledby]="multiViewCalendarHeaderIdLabel"
[activeView]="activeViewEnum"
[activeDescendant]="ariaActivedescendant"
[isActive]="isActive || isHovered"
[cellTemplateRef]="activeCellTemplate()?.templateRef"
[weekNumberTemplateRef]="weekNumberTemplateRef?.templateRef"
[cellUID]="cellUID"
[weekDaysFormat]="weekDaysFormat"
[views]="views"
[min]="min"
[max]="max"
[focusedDate]="focusedDate"
[animateNavigation]="animateNavigation"
[showViewHeader]="showViewHeader"
[weekNumber]="weekNumber"
[activeRangeEnd]="activeRangeEnd"
[selectionRange]="selectionRange"
[selectedDates]="selectedDates"
[orientation]="orientation"
[tabIndex]="tabIndex"
[disabled]="disabled"
(cellClick)="handleCellClick($event)"
(weekNumberCellClick)="handleWeekNumberClick($event)"
(cellEnter)="onCellEnter(cellEnter, $event)"
(cellLeave)="emitCellEvent(cellLeave, $event)"
(activeDateChange)="setActiveDate($event)"
(focusCalendar)="handleFocus()"
(blurCalendar)="handleFocusout($event)"
>
</kendo-calendar-horizontal>
<kendo-calendar-footer
*ngIf="footer"
[footerTemplateRef]="footerTemplateRef?.templateRef"
[activeViewValue]="activeView"
[currentDate]="activeDate">