@progress/kendo-angular-scheduler
Version:
Kendo UI Scheduler Angular - Outlook or Google-style angular scheduler calendar. Full-featured and customizable embedded scheduling from the creator developers trust for professional UI components.
433 lines (432 loc) • 22.3 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 { ChangeDetectorRef, Component, ElementRef, Input, NgZone, Renderer2, ViewChild, } from '@angular/core';
import { ScrollbarWidthService } from '@progress/kendo-angular-common';
import { ViewContextService } from '../view-context.service';
import { ViewStateService } from '../view-state.service';
import { IntlService } from '@progress/kendo-angular-intl';
import { PDFService } from '../../pdf/pdf.service';
import { LocalizationService } from '@progress/kendo-angular-l10n';
import { addDays, addYears, toLocalDate } from '@progress/kendo-date-math';
import { TooltipDirective } from '@progress/kendo-angular-tooltip';
import { formatEventTime, toUTCDate } from '../utils';
import { caretAltLeftIcon, caretAltRightIcon } from '@progress/kendo-svg-icons';
import { FocusService } from '../../navigation';
import { MultiViewCalendarComponent, MonthCellTemplateDirective } from '@progress/kendo-angular-dateinputs';
import { BaseView } from '../common/base-view';
import { createTasks, noop, yearEnd, yearStart } from './utils';
import { MonthSlotService } from '../month/month-slot.service';
import { IconWrapperComponent } from '@progress/kendo-angular-icons';
import { NgIf, NgFor, NgClass, NgStyle } from '@angular/common';
import { take } from 'rxjs/operators';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
import * as i2 from "../../navigation";
import * as i3 from "@progress/kendo-angular-intl";
import * as i4 from "../view-context.service";
import * as i5 from "../view-state.service";
import * as i6 from "../../pdf/pdf.service";
import * as i7 from "../month/month-slot.service";
import * as i8 from "@progress/kendo-angular-common";
const today = new Date(Date.now());
const getDateAttribute = (element) => element?.querySelector('span>span[date]')?.getAttribute('date');
/**
* @hidden
*/
export class YearViewInternalComponent extends BaseView {
localization;
focusService;
intl;
/**
* Calculates the next or previous range to be displayed
*/
newRange;
/**
* Determines the displayed date range and formats the selected date
*/
dateRangeFn;
calendar;
tooltip;
get arrowIcons() {
return !this.localization.rtl
? ['caret-alt-left', 'caret-alt-right']
: ['caret-alt-right', 'caret-alt-left'];
}
get arrowSVGIcons() {
return !this.localization.rtl
? [this.caretAltLeftIcon, this.caretAltRightIcon]
: [this.caretAltRightIcon, this.caretAltLeftIcon];
}
caretAltLeftIcon = caretAltLeftIcon;
caretAltRightIcon = caretAltRightIcon;
eventsPerSelectedDay = [];
days = [];
focusedDate;
currentTd;
tds = [];
isTooltipClicked = false;
constructor(localization, focusService, intl, viewContext, viewState, zone, renderer, pdfService, element, slotService, scrollBarWidthService, changeDetector) {
super(viewContext, viewState, intl, slotService, zone, renderer, element, pdfService, localization, changeDetector, scrollBarWidthService);
this.localization = localization;
this.focusService = focusService;
this.intl = intl;
}
ngAfterViewInit() {
this.updateTds();
super.ngAfterViewInit();
this.focusedDate = new Date(this.selectedDate.getFullYear(), today.getMonth(), today.getDate());
}
getSlotClass(date) {
if (this.slotClass) {
return this.slotClass({
start: date,
end: addDays(date, 1),
events: this.eventsPerDay(date),
});
}
}
getEventClasses(item, resources, isAllDay) {
if (this.eventClass) {
return this.eventClass({
event: item.event,
resources,
isAllDay,
});
}
}
onBlur() {
if (!this.isTooltipClicked) {
this.tooltip.hide();
}
this.isTooltipClicked = false;
}
getDate(element) {
return new Date(getDateAttribute(element.nativeElement));
}
eventTitle(event) {
const startTime = toLocalDate(event.startTime);
const endTime = toLocalDate(event.endTime);
const time = formatEventTime(startTime, endTime, event.isAllDay, this.intl.localeId);
return `${time}, ${event.event.title}`;
}
onClick(event) {
if (event.target.tagName === 'SPAN') {
const clickedDate = getDateAttribute(event.target.closest('td.k-calendar-td'));
if (clickedDate === getDateAttribute(this.currentTd)) {
this.tooltip.show(this.currentTd);
}
}
else {
this.tooltip.hide();
}
}
onMouseDown() {
this.isTooltipClicked = true;
}
onKeydown(event) {
if (event.key === 'Backspace' || event.key === 'Delete') {
this.tooltip.hide();
}
if (event.key === 'Enter' && !this.tooltip.popupRef) {
this.tooltip.show(this.currentTd);
return;
}
if (event.key === 'Enter' && this.tooltip.popupRef && (getDateAttribute(this.currentTd) === this.calendar.focusedDate.toString())) {
this.navigateToDay(new Date(getDateAttribute(this.currentTd)));
}
}
navigateToDay(date) {
this.tooltip.hide();
this.zone.run(() => {
this.viewState.navigateTo({ viewName: 'day', date: new Date(date) });
});
}
eventsPerDay(date) {
return this.tasksPerDay(date)?.map((task) => task.event) || [];
}
tasksPerDay(date) {
if (this.resources?.length > 0) {
return this.tasks?.filter((event) => event.resources.length > 0 &&
event.startTime.getUTCDate() === date.getDate() &&
event.startTime.getUTCMonth() === date.getMonth());
}
else {
return this.tasks?.filter((event) => event.startTime.getUTCDate() === date.getDate() && event.startTime.getUTCMonth() === date.getMonth());
}
}
onValueChange(date) {
this.eventsPerSelectedDay = this.tasksPerDay(date);
this.currentTd = this.tds.find((td) => getDateAttribute(td) === date.toString());
if (this.tooltip.popupRef) {
this.tooltip.hide();
}
}
hasEvent(date) {
return this.tasksPerDay(date).length > 0;
}
createPDFElement() {
const element = this.element.nativeElement.cloneNode(true);
element.style.width = `${this.element.nativeElement.offsetWidth}px`;
element.querySelector('.k-scheduler-layout').style.height = 'auto';
this.pdfService.elementReady.emit({
element: element,
});
}
onSelectDate(date) {
const year = date.getFullYear();
const start = yearStart(year);
this.focusedDate = new Date(year, start.getMonth(), today.getDate());
this.selectedDate = start;
const dateRange = this.dateRange(date);
this.viewState.notifyDateRange(dateRange);
this.days = this.createDaySlots(dateRange);
if (this.calendar) {
this.calendar.min = start;
this.calendar.max = yearEnd(start.getFullYear());
// wait for the view to update to get the new tds
this.zone.onStable.pipe(take(1)).subscribe(() => this.updateTds());
}
}
onAction(e) {
const now = this.selectedDate;
if (e.type === 'next' || e.type === 'prev') {
const offset = e.type === 'next' ? 1 : -1;
const next = addYears(now, offset);
this.viewState.notifyNextDate(next);
this.calendar.min = next;
this.calendar.max = yearEnd(next.getFullYear());
this.focusedDate = new Date(next.getFullYear(), today.getMonth(), today.getDate());
}
}
createTasks(items, dateRange) {
this.days = this.createDaySlots(dateRange);
return createTasks(dateRange.start, dateRange.end, items, this.days);
}
reflow() {
this.updateContentHeight();
const content = this.content.nativeElement;
if (this.contentHeight === 'auto') {
// bigger size changes cause the table to overflow the container and in horizontal scrollbars
// this changes the table and slots size during rendering before the browser re-adjusts the 100% table width
content.style.overflow = 'visible';
}
if (this.contentHeight === 'auto') {
content.style.overflow = '';
}
}
dateRange(date = this.selectedDate) {
return this.dateRangeFn(date);
}
onTasksChange() {
this.items.next(this.tasks);
}
slotByIndex = (slotIndex, _args) => noop(slotIndex);
dragHintSize = (startSlot, _endSlot) => noop(startSlot);
dragRanges = (slot) => noop(slot);
slotByPosition = (x, _y, _container) => noop(x);
createDaySlots({ start }) {
const days = [];
const monthsPerYear = 12;
let date = start;
for (let idx = 0; idx < monthsPerYear; idx++) {
const monthTotalDays = this.getLastDayOfMonth(date.getFullYear(), date.getMonth());
for (let dayIdx = 0; dayIdx < monthTotalDays; dayIdx++) {
days.push(date);
const nextDay = addDays(date, 1);
date = nextDay;
}
}
return days;
}
getLastDayOfMonth(year, month) {
return new Date(year, month + 1, 0).getDate();
}
cachedTds = [];
updateTds() {
this.cachedTds.forEach((td) => {
this.renderer.setAttribute(td, 'class', 'k-calendar-td');
});
this.cachedTds = [];
this.tds = Array.from(this.calendar.element.nativeElement.querySelectorAll('.k-calendar-td:not(.k-empty)'));
if (this.calendar.value instanceof Date &&
this.calendar.value.getFullYear() === this.calendar.min?.getFullYear()) {
this.currentTd = this.tds.find((td) => getDateAttribute(td) === this.calendar.value.toString());
}
if (this.slotClass) {
this.tds.forEach((td) => {
const date = toUTCDate(new Date(getDateAttribute(td)));
const userClass = this.getSlotClass(date);
if (userClass) {
this.renderer.addClass(td, userClass);
this.cachedTds.push(td);
}
});
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: YearViewInternalComponent, deps: [{ token: i1.LocalizationService }, { token: i2.FocusService }, { token: i3.IntlService }, { token: i4.ViewContextService }, { token: i5.ViewStateService }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i6.PDFService }, { token: i0.ElementRef }, { token: i7.MonthSlotService }, { token: i8.ScrollbarWidthService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: YearViewInternalComponent, isStandalone: true, selector: "year-view-internal", inputs: { newRange: "newRange", dateRangeFn: "dateRangeFn" }, providers: [MonthSlotService], viewQueries: [{ propertyName: "calendar", first: true, predicate: MultiViewCalendarComponent, descendants: true }, { propertyName: "tooltip", first: true, predicate: TooltipDirective, descendants: true }], usesInheritance: true, ngImport: i0, template: `
<div #content class="k-scheduler-layout k-scheduler-layout-flex k-scheduler-yearview">
<div class="k-scheduler-body">
<kendo-multiviewcalendar
[showOtherMonthDays]="false"
[showCalendarHeader]="false"
[showViewHeader]="true"
[views]="12"
[focusedDate]="focusedDate"
kendoTooltip
filter=".k-calendar-td"
showOn="none"
[tooltipTemplate]="template"
position="right"
tooltipContentClass="k-scheduler-tooltip"
[tooltipWidth]="220"
[collision]="{ horizontal: 'flip', vertical: 'flip' }"
(valueChange)="onValueChange($event)"
(blur)="onBlur()"
>
<ng-template kendoCalendarMonthCellTemplate let-date let-context="cellContext">
<span *ngIf="!context.isOtherMonth" [attr.date]="date">{{ date.getDate() }}</span>
<span *ngIf="!context.isOtherMonth && hasEvent(date)" class="k-day-indicator"></span>
</ng-template>
</kendo-multiviewcalendar>
</div>
</div>
<ng-template #template let-anchor>
<div
class="k-tooltip-title k-text-center"
(click)="navigateToDay(getDate(anchor))"
(mousedown)="onMouseDown()"
>
<div class="k-month">{{ intl.formatDate(getDate(anchor), 'MMM') }}</div>
<div class="k-link k-day k-text-primary">{{ intl.formatDate(getDate(anchor), 'dd') }}</div>
</div>
<div class="k-tooltip-events-container" (mousedown)="onMouseDown()">
<div class="k-tooltip-events">
<div
*ngFor="let event of eventsPerSelectedDay"
class="k-tooltip-event k-event"
[title]="eventTitle(event)"
[ngClass]="getEventClasses(event, event.resources)"
[ngStyle]="getEventStyles(event, event.resources[0], event.isAllDay)"
>
<kendo-icon-wrapper
*ngIf="event.tail || event.mid"
[name]="arrowIcons[0]"
[svgIcon]="arrowSVGIcons[0]"
>
</kendo-icon-wrapper>
<div class="k-event-title k-text-ellipsis">{{ event.event.title }}</div>
<span class="k-spacer"></span>
<span
class="k-event-time"
*ngIf="(event.isMultiDay && event.head && !event.isAllDay) || !event.isMultiDay"
>{{ intl.formatDate(event.start, 't') }}</span
>
<kendo-icon-wrapper
*ngIf="event.head || event.mid"
[name]="arrowIcons[1]"
[svgIcon]="arrowSVGIcons[1]"
>
</kendo-icon-wrapper>
</div>
</div>
</div>
<div *ngIf="eventsPerSelectedDay.length === 0" class="k-no-data k-text-center">
{{ localization.get('yearViewNoEvents') }}
</div>
</ng-template>
`, isInline: true, dependencies: [{ kind: "component", type: MultiViewCalendarComponent, selector: "kendo-multiviewcalendar", inputs: ["showOtherMonthDays", "showCalendarHeader", "size", "id", "focusedDate", "footer", "min", "max", "rangeValidation", "disabledDatesRangeValidation", "selection", "allowReverse", "value", "disabled", "tabindex", "tabIndex", "weekDaysFormat", "isActive", "disabledDates", "activeView", "bottomView", "topView", "showViewHeader", "animateNavigation", "weekNumber", "activeRangeEnd", "selectionRange", "views", "orientation", "cellTemplate", "monthCellTemplate", "yearCellTemplate", "decadeCellTemplate", "centuryCellTemplate", "weekNumberTemplate", "footerTemplate", "headerTitleTemplate", "headerTemplate"], outputs: ["activeViewChange", "navigate", "cellEnter", "cellLeave", "valueChange", "rangeSelectionChange", "blur", "focus", "focusCalendar", "onClosePopup", "onTabPress", "onShiftTabPress"], exportAs: ["kendo-multiviewcalendar"] }, { kind: "directive", type: TooltipDirective, selector: "[kendoTooltip]", inputs: ["filter", "position", "titleTemplate", "showOn", "showAfter", "callout", "closable", "offset", "tooltipWidth", "tooltipHeight", "tooltipClass", "tooltipContentClass", "collision", "closeTitle", "tooltipTemplate"], exportAs: ["kendoTooltip"] }, { kind: "directive", type: MonthCellTemplateDirective, selector: "[kendoCalendarMonthCellTemplate]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: YearViewInternalComponent, decorators: [{
type: Component,
args: [{
selector: 'year-view-internal',
providers: [MonthSlotService],
template: `
<div #content class="k-scheduler-layout k-scheduler-layout-flex k-scheduler-yearview">
<div class="k-scheduler-body">
<kendo-multiviewcalendar
[showOtherMonthDays]="false"
[showCalendarHeader]="false"
[showViewHeader]="true"
[views]="12"
[focusedDate]="focusedDate"
kendoTooltip
filter=".k-calendar-td"
showOn="none"
[tooltipTemplate]="template"
position="right"
tooltipContentClass="k-scheduler-tooltip"
[tooltipWidth]="220"
[collision]="{ horizontal: 'flip', vertical: 'flip' }"
(valueChange)="onValueChange($event)"
(blur)="onBlur()"
>
<ng-template kendoCalendarMonthCellTemplate let-date let-context="cellContext">
<span *ngIf="!context.isOtherMonth" [attr.date]="date">{{ date.getDate() }}</span>
<span *ngIf="!context.isOtherMonth && hasEvent(date)" class="k-day-indicator"></span>
</ng-template>
</kendo-multiviewcalendar>
</div>
</div>
<ng-template #template let-anchor>
<div
class="k-tooltip-title k-text-center"
(click)="navigateToDay(getDate(anchor))"
(mousedown)="onMouseDown()"
>
<div class="k-month">{{ intl.formatDate(getDate(anchor), 'MMM') }}</div>
<div class="k-link k-day k-text-primary">{{ intl.formatDate(getDate(anchor), 'dd') }}</div>
</div>
<div class="k-tooltip-events-container" (mousedown)="onMouseDown()">
<div class="k-tooltip-events">
<div
*ngFor="let event of eventsPerSelectedDay"
class="k-tooltip-event k-event"
[title]="eventTitle(event)"
[ngClass]="getEventClasses(event, event.resources)"
[ngStyle]="getEventStyles(event, event.resources[0], event.isAllDay)"
>
<kendo-icon-wrapper
*ngIf="event.tail || event.mid"
[name]="arrowIcons[0]"
[svgIcon]="arrowSVGIcons[0]"
>
</kendo-icon-wrapper>
<div class="k-event-title k-text-ellipsis">{{ event.event.title }}</div>
<span class="k-spacer"></span>
<span
class="k-event-time"
*ngIf="(event.isMultiDay && event.head && !event.isAllDay) || !event.isMultiDay"
>{{ intl.formatDate(event.start, 't') }}</span
>
<kendo-icon-wrapper
*ngIf="event.head || event.mid"
[name]="arrowIcons[1]"
[svgIcon]="arrowSVGIcons[1]"
>
</kendo-icon-wrapper>
</div>
</div>
</div>
<div *ngIf="eventsPerSelectedDay.length === 0" class="k-no-data k-text-center">
{{ localization.get('yearViewNoEvents') }}
</div>
</ng-template>
`,
standalone: true,
imports: [MultiViewCalendarComponent, TooltipDirective, MonthCellTemplateDirective, NgIf, NgFor, NgClass, NgStyle, IconWrapperComponent]
}]
}], ctorParameters: function () { return [{ type: i1.LocalizationService }, { type: i2.FocusService }, { type: i3.IntlService }, { type: i4.ViewContextService }, { type: i5.ViewStateService }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i6.PDFService }, { type: i0.ElementRef }, { type: i7.MonthSlotService }, { type: i8.ScrollbarWidthService }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { newRange: [{
type: Input
}], dateRangeFn: [{
type: Input
}], calendar: [{
type: ViewChild,
args: [MultiViewCalendarComponent]
}], tooltip: [{
type: ViewChild,
args: [TooltipDirective]
}] } });