UNPKG

@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.

421 lines (420 loc) 19.2 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, Input, ViewChild, ElementRef, NgZone, Renderer2, TemplateRef } from '@angular/core'; import { anyChanged } from '@progress/kendo-angular-common'; import { IntlService } from '@progress/kendo-angular-intl'; import { addDays, getDate } from '@progress/kendo-date-math'; import { Subscription, BehaviorSubject, combineLatest, fromEvent, merge } from 'rxjs'; import { switchMap, take, map } from 'rxjs/operators'; import { ViewContextService } from '../view-context.service'; import { ViewStateService } from '../view-state.service'; import { hasScrollbar, closestInScope, hasClasses } from '../../common/dom-queries'; import { createResourceGroups, groupEvents } from './utils'; import { ignoreContentChild, elementOffset } from '../utils'; import { PDFService } from '../../pdf/pdf.service'; import { LocalizationService } from '@progress/kendo-angular-l10n'; import { AsyncPipe } from '@angular/common'; import { AgendaListComponent } from './agenda-view-list.component'; import { AgendaHeaderComponent } from './agenda-header.component'; import * as i0 from "@angular/core"; import * as i1 from "../view-context.service"; import * as i2 from "../view-state.service"; import * as i3 from "@progress/kendo-angular-intl"; import * as i4 from "../../pdf/pdf.service"; import * as i5 from "@progress/kendo-angular-l10n"; /** * @hidden */ export class AgendaViewInternalComponent { viewContext; viewState; intl; renderer; element; zone; pdfService; localization; selectedDate; eventTemplate; slotClass; eventClass; eventStyles; agendaTimeTemplate; agendaDateTemplate; selectedDateFormat; selectedShortDateFormat; numberOfDays; headerWrap; content; tasks = new BehaviorSubject(null); groupedResources = []; editable; get eventTemplateRef() { return this.eventTemplate || (this.schedulerEventTemplate || {}).templateRef; } get agendaTimeTemplateRef() { return this.agendaTimeTemplate || (this.schedulerAgendaTimeTemplate || {}).templateRef; } get agendaDateTemplateRef() { return this.agendaDateTemplate || (this.schedulerAgendaDateTemplate || {}).templateRef; } schedulerEventTemplate; schedulerAgendaTimeTemplate; schedulerAgendaDateTemplate; group; resources; spans = []; items; range; groups; min; max; subs = new Subscription(); constructor(viewContext, viewState, intl, renderer, element, zone, pdfService, localization) { this.viewContext = viewContext; this.viewState = viewState; this.intl = intl; this.renderer = renderer; this.element = element; this.zone = zone; this.pdfService = pdfService; this.localization = localization; } ngOnInit() { this.updateContentHeight = this.updateContentHeight.bind(this); this.subs.add(this.viewContext.selectedDate.subscribe(this.onSelectDate.bind(this))); this.subs.add(this.viewContext.action.subscribe(this.onAction.bind(this))); this.subs.add(this.viewContext.execute.subscribe(this.execute.bind(this))); this.subs.add(this.viewContext.resize.subscribe(this.updateContentHeight)); this.subs.add(combineLatest([ this.viewContext.items, this.viewState.dateRange ]).pipe(map(([items, dateRange]) => { this.items = items; this.range = dateRange; return this.createEventGroups(); })) .subscribe((tasks) => { this.tasks.next(tasks); })); this.subs.add(this.viewContext.optionsChange.subscribe(this.optionsChange.bind(this))); const onStable = () => this.zone.onStable.pipe(take(1)); this.subs.add(combineLatest(this.tasks, this.localization.changes).pipe(switchMap(onStable)) .subscribe(this.updateContentHeight)); this.subs.add(this.pdfService.createElement.subscribe(this.createPDFElement.bind(this))); } ngOnChanges(changes) { if (anyChanged(['selectedDateFormat', 'selectedShortDateFormat', 'numberOfDays'], changes)) { this.viewState.notifyDateRange(this.dateRange(this.selectedDate)); } } ngAfterViewInit() { if (!this.element) { return; } const contentElement = this.content.nativeElement; this.zone.runOutsideAngular(() => { this.subs.add(merge(fromEvent(contentElement, 'click'), fromEvent(contentElement, 'contextmenu'), fromEvent(contentElement, 'dblclick')) .subscribe(e => this.onClick(e))); this.subs.add(fromEvent(contentElement, 'keydown') .subscribe(e => this.onKeydown(e))); }); } onClick(e) { const targetTask = this.targetTask(e.target); if (targetTask) { const { task, eventTarget } = targetTask; const eventType = e.type; const isSingle = eventType === 'click'; const isDouble = eventType === 'dblclick'; if (isSingle && closestInScope(e.target, node => hasClasses(node, 'k-event-delete'), eventTarget)) { this.viewState.emitEvent('remove', { event: task.event, dataItem: task.event.dataItem }); } else { const name = isDouble ? 'eventDblClick' : 'eventClick'; this.viewState.emitEvent(name, { type: eventType, event: task.event, originalEvent: e }); } } } onKeydown(e) { const targetTask = this.targetTask(e.target); if (targetTask) { const task = targetTask.task; this.viewState.emitEvent('eventKeydown', { event: task.event, dataItem: task.event.dataItem, originalEvent: e }); } } targetTask(target) { const eventTarget = closestInScope(target, node => node.hasAttribute('data-task-index'), this.element.nativeElement); if (eventTarget) { return { eventTarget, task: this.elementTask(eventTarget) }; } } updateContentHeight() { const element = this.element.nativeElement; const parent = element.parentNode; const content = this.content.nativeElement; this.renderer.setStyle(content, 'height', ''); let height = parent.clientHeight; for (let idx = 0; idx < parent.children.length; idx++) { const child = parent.children[idx]; if (child !== element && !ignoreContentChild(child)) { height -= child.offsetHeight; } } const headerElement = this.headerWrap.nativeElement; height -= this.headerWrap ? headerElement.offsetHeight : 0; this.renderer.setStyle(content, 'height', `${height}px`); const rtl = this.localization.rtl; // Need to explicitly set 'padding-inline-xxx' to 0px when the Scheduler has no height set if (!hasScrollbar(content, 'vertical')) { this.renderer.setStyle(headerElement, !rtl ? 'padding-inline-end' : 'padding-inline-start', '0px'); } this.renderer.removeStyle(headerElement, rtl ? 'padding-inline-end' : 'padding-inline-start'); this.viewState.notifyLayoutEnd(); } ngOnDestroy() { this.subs.unsubscribe(); } optionsChange(changes) { this.group = changes.group; this.resources = changes.resources; this.groupResources(); this.min = changes.min; this.max = changes.max; this.editable = changes.editable; if (this.items && this.items.length) { this.tasks.next(this.createEventGroups()); } this.schedulerEventTemplate = changes.eventTemplate; this.schedulerAgendaTimeTemplate = changes.agendaTimeTemplate; this.schedulerAgendaDateTemplate = changes.agendaDateTemplate; } onSelectDate(date) { this.selectedDate = date; this.viewState.notifyDateRange(this.dateRange()); } onAction(e) { const now = getDate(this.selectedDate); if (e.type === 'next') { const next = getDate(addDays(now, this.numberOfDays)); if (this.isInRange(next)) { this.viewState.notifyNextDate(next); } } if (e.type === 'prev') { const next = getDate(addDays(now, -this.numberOfDays)); if (this.isInRange(next)) { this.viewState.notifyNextDate(next); } } } createEventGroups() { const resourceGroups = this.groupedResources.length ? createResourceGroups(this.groupedResources) : null; const eventGroups = this.groups = groupEvents(this.items, { taskResources: this.taskResources, resourceGroups, allResources: this.resources, spans: this.spans, dateRange: this.range }); return eventGroups; } dateRange(date = this.selectedDate) { const start = getDate(date); const end = getDate(addDays(start, this.numberOfDays)); const rangeEnd = getDate(addDays(start, this.numberOfDays - 1)); const text = this.intl.format(this.selectedDateFormat, start, rangeEnd); const shortText = this.intl.format(this.selectedShortDateFormat, start, rangeEnd); return { start, end, text, shortText }; } groupResources() { const resources = this.resources || []; const group = this.group || {}; const grouped = group.resources; const groupedResources = this.groupedResources = []; if (grouped && grouped.length) { for (let groupIdx = 0; groupIdx < grouped.length; groupIdx++) { const name = grouped[groupIdx]; const resource = resources.find(item => item.name === name); if (resource) { groupedResources.push(resource); } } } this.spans = this.resourceSpans(); } resourceSpans() { const spans = [1]; const resources = this.groupedResources; let span = 1; for (let idx = resources.length - 1; idx > 0; idx--) { span *= ((resources[idx].data || []).length || 1); spans.unshift(span); } return spans; } get taskResources() { if (this.groupedResources.length) { return this.groupedResources; } else if (this.resources && this.resources.length) { return [this.resources[0]]; } else { return [{}]; } } isInRange(date) { const dateRange = this.dateRange(date); return (!this.min || this.min < dateRange.end) && (!this.max || dateRange.start <= this.max); } createPDFElement() { const element = this.element.nativeElement.cloneNode(true); element.style.width = `${this.element.nativeElement.offsetWidth}px`; element.querySelector('.k-scheduler-content').style.height = 'auto'; const header = element.querySelector('.k-scheduler-header'); header.style.paddingRight = 0; header.style.paddingLeft = 0; this.pdfService.elementReady.emit({ element: element }); } elementTask(element) { const index = parseInt(element.getAttribute('data-task-index'), 10); const groupIndex = parseInt(element.getAttribute('data-group-index'), 10); const group = this.groups[groupIndex]; const task = group.tasks.itemAt(index); return task; } execute(e) { if (e.name === 'slotByPosition') { const slot = this.slotByPosition(e.args); e.result(slot); } else if (e.name === 'eventFromElement') { const task = this.elementTask(e.args.element); if (task) { e.result(task.event); } } } slotByPosition({ x, y }) { const contentTable = this.content.nativeElement.querySelector('table'); const offset = elementOffset(contentTable); if (offset.top <= y && y <= offset.top + offset.height) { const contentRows = contentTable.rows; if (!contentRows.length) { return; } const taskOffset = elementOffset(contentRows[0].cells[contentRows[0].cells.length - 1]); if (taskOffset.left <= x && x <= taskOffset.left + taskOffset.width) { for (let idx = 0; idx < contentRows.length; idx++) { const row = contentRows[idx]; const rowOffset = elementOffset(row); if (rowOffset.top <= y && y <= rowOffset.top + rowOffset.height) { const element = row.querySelector('[data-task-index]'); const task = this.elementTask(element); const event = task.event; return { element: new ElementRef(element), start: event.start, end: event.end, event: event, resources: task.resources, isAllDay: task.isAllDay }; } } } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AgendaViewInternalComponent, deps: [{ token: i1.ViewContextService }, { token: i2.ViewStateService }, { token: i3.IntlService }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.NgZone }, { token: i4.PDFService }, { token: i5.LocalizationService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: AgendaViewInternalComponent, isStandalone: true, selector: "agenda-view-internal", inputs: { eventTemplate: "eventTemplate", slotClass: "slotClass", eventClass: "eventClass", eventStyles: "eventStyles", agendaTimeTemplate: "agendaTimeTemplate", agendaDateTemplate: "agendaDateTemplate", selectedDateFormat: "selectedDateFormat", selectedShortDateFormat: "selectedShortDateFormat", numberOfDays: "numberOfDays" }, viewQueries: [{ propertyName: "headerWrap", first: true, predicate: ["headerWrap"], descendants: true, read: ElementRef, static: true }, { propertyName: "content", first: true, predicate: ["content"], descendants: true, read: ElementRef, static: true }], usesOnChanges: true, ngImport: i0, template: ` <table class="k-scheduler-layout k-scheduler-agendaview" role="grid"> <tbody role="none"> <tr class="k-scheduler-head"> <td> <div kendoSchedulerAgendaHeader [resources]="groupedResources" #headerWrap></div> </td> </tr> <tr class="k-scheduler-body"> <td> <div kendoSchedulerAgendaList #content [editable]="editable" [eventTemplate]="eventTemplateRef" [slotClass]="slotClass" [eventClass]="eventClass" [eventStyles]="eventStyles" [agendaTimeTemplate]="agendaTimeTemplateRef" [agendaDateTemplate]="agendaDateTemplateRef" [tasks]="tasks | async"> </div> </td> </tr> </tbody> </table> `, isInline: true, dependencies: [{ kind: "component", type: AgendaHeaderComponent, selector: "[kendoSchedulerAgendaHeader]", inputs: ["resources"] }, { kind: "component", type: AgendaListComponent, selector: "[kendoSchedulerAgendaList]", inputs: ["tasks", "eventTemplate", "slotClass", "eventClass", "eventStyles", "agendaTimeTemplate", "agendaDateTemplate", "editable"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AgendaViewInternalComponent, decorators: [{ type: Component, args: [{ // eslint-disable-next-line @angular-eslint/component-selector selector: 'agenda-view-internal', template: ` <table class="k-scheduler-layout k-scheduler-agendaview" role="grid"> <tbody role="none"> <tr class="k-scheduler-head"> <td> <div kendoSchedulerAgendaHeader [resources]="groupedResources" #headerWrap></div> </td> </tr> <tr class="k-scheduler-body"> <td> <div kendoSchedulerAgendaList #content [editable]="editable" [eventTemplate]="eventTemplateRef" [slotClass]="slotClass" [eventClass]="eventClass" [eventStyles]="eventStyles" [agendaTimeTemplate]="agendaTimeTemplateRef" [agendaDateTemplate]="agendaDateTemplateRef" [tasks]="tasks | async"> </div> </td> </tr> </tbody> </table> `, standalone: true, imports: [AgendaHeaderComponent, AgendaListComponent, AsyncPipe] }] }], ctorParameters: function () { return [{ type: i1.ViewContextService }, { type: i2.ViewStateService }, { type: i3.IntlService }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i0.NgZone }, { type: i4.PDFService }, { type: i5.LocalizationService }]; }, propDecorators: { eventTemplate: [{ type: Input }], slotClass: [{ type: Input }], eventClass: [{ type: Input }], eventStyles: [{ type: Input }], agendaTimeTemplate: [{ type: Input }], agendaDateTemplate: [{ type: Input }], selectedDateFormat: [{ type: Input }], selectedShortDateFormat: [{ type: Input }], numberOfDays: [{ type: Input }], headerWrap: [{ type: ViewChild, args: ['headerWrap', { read: ElementRef, static: true }] }], content: [{ type: ViewChild, args: ['content', { read: ElementRef, static: true }] }] } });