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,272 lines (1,268 loc) 62 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, 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">