@progress/kendo-angular-gantt
Version:
Kendo UI Angular Gantt
224 lines (223 loc) • 11.1 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, Directive, ElementRef, HostBinding, inject, Input, isDevMode, ViewChild } from '@angular/core';
import { MS_PER_HOUR, MS_PER_DAY, firstDayOfMonth } from '@progress/kendo-date-math';
import { Subscription } from 'rxjs';
import { NavigationService } from '../navigation/navigation.service';
import { OptionChangesService } from '../common/option-changes.service';
import { TimelineViewService } from '../timeline/timeline-view.service';
import { DependencyDomService } from '../dependencies/dependency-dom.service';
import { MappingService } from '../common/mapping.service';
import { getTotalDaysInMonth, getTotalMonthsInBetween, isNumber, isPresent } from '../utils';
import { TaskDragService } from '../dragging/task-drag.service';
import * as i0 from "@angular/core";
import * as i1 from "../common/mapping.service";
import * as i2 from "../timeline/timeline-view.service";
import * as i3 from "../dependencies/dependency-dom.service";
import * as i4 from "../common/option-changes.service";
import * as i5 from "../navigation/navigation.service";
const slotUnitDuration = {
day: MS_PER_HOUR,
week: MS_PER_DAY,
month: MS_PER_DAY * 7
};
const FOCUSED_CLASS = 'k-focus';
/**
* @hidden
*/
export class GanttTaskBase {
mapper;
timelineViewService;
dependencyDomService;
optionChangesService;
cdr;
navigationService;
wrapperClass = true;
get taskIndexAttribute() {
return this.index;
}
/**
* Points to the `.k-task` element of the template (present in all three task types).
*/
taskElement;
dataItem;
index;
level;
renderDependencyDragClues;
selectable;
isSelected;
// better to be set in a service and retrieved from it whenever needed?
activeView;
taskClass;
get ariaSelected() {
// assigning null will not render the attribute at all (desired in selectable="false" mode)
return this.selectable ? String(this.isSelected(this.dataItem)) : null;
}
get slotUnitDuration() {
return slotUnitDuration[this.activeView];
}
get viewService() {
return this.timelineViewService.service(this.activeView);
}
get slotWidth() {
return this.viewService.options.slotWidth;
}
get taskWidth() {
const taskStart = this.mapper.extractFromTask(this.dataItem, 'start');
const taskEnd = this.mapper.extractFromTask(this.dataItem, 'end');
if (this.activeView === 'year') {
if (!taskStart || !taskEnd) {
return;
}
const monthsDiff = Math.max(getTotalMonthsInBetween(taskStart, taskEnd), 0);
const totalDaysInStartMonth = getTotalDaysInMonth(taskStart);
if (monthsDiff > 0 || (monthsDiff === 0 && taskStart.getMonth() !== taskEnd.getMonth())) {
const startFraction = (totalDaysInStartMonth - taskStart.getDate()) / totalDaysInStartMonth;
const endFraction = taskEnd.getDate() / getTotalDaysInMonth(taskEnd);
return (startFraction + monthsDiff + endFraction) * this.slotWidth;
}
else {
const fraction = (taskEnd.getDate() - taskStart.getDate()) / totalDaysInStartMonth;
return fraction * this.slotWidth;
}
}
const itemDuration = taskEnd - taskStart;
const durationInSlotUnits = itemDuration / this.slotUnitDuration;
const width = durationInSlotUnits * this.slotWidth;
return width;
}
/**
* The `left` style prop has to be applied to the host element (.k-task-wrap), as the drag clue elements are displayed on .k-task-wrap hover.
* Applying the `left` offset to the inner .k-task element leaves the .k-task-wrap element rendered with an offset of 0 somewhere on the left
* and hovering just the .k-task element doesn't expose the drag clues.
* Additionally, positioning the entire container takes care of positioning the hints as well.
*/
get taskOffset() {
const taskStart = this.mapper.extractFromTask(this.dataItem, 'start');
const taskEnd = this.mapper.extractFromTask(this.dataItem, 'end');
const viewStart = this.viewService.viewStart;
if (this.activeView === 'year') {
if (!taskStart || !taskEnd) {
return;
}
const viewStartDate = new Date(viewStart);
const offsetSlots = getTotalMonthsInBetween(viewStartDate, new Date(firstDayOfMonth(taskStart))) + 1;
const currentMonthOffset = taskStart.getDate() / getTotalDaysInMonth(taskStart);
return (offsetSlots + currentMonthOffset) * this.slotWidth;
}
const timeAfterViewStart = taskStart - viewStart;
const offsetInSlotUnits = timeAfterViewStart / this.slotUnitDuration;
const offset = offsetInSlotUnits * this.slotWidth;
return offset;
}
get completionOverlayWidth() {
const overlayWidth = this.taskWidth * this.mapper.extractFromTask(this.dataItem, 'completionRatio');
// fall-back to 0 in case no completionRatio is provided
return isNumber(overlayWidth) ? overlayWidth : 0;
}
draggedCompletionWidth;
taskDragService;
completionDragResult;
subscriptions = new Subscription();
constructor(mapper, // left public to be available for usage in the templates
timelineViewService, dependencyDomService, optionChangesService, cdr, navigationService) {
this.mapper = mapper;
this.timelineViewService = timelineViewService;
this.dependencyDomService = dependencyDomService;
this.optionChangesService = optionChangesService;
this.cdr = cdr;
this.navigationService = navigationService;
this.taskDragService = inject(TaskDragService, { optional: true });
this.subscriptions.add(this.optionChangesService.viewChanges
.subscribe(() => this.cdr.markForCheck()));
this.subscriptions.add(this.navigationService.taskStatusChanges
.subscribe(this.updateActiveState.bind(this)));
}
ngOnInit() {
const taskStart = this.mapper.extractFromTask(this.dataItem, 'start');
const taskEnd = this.mapper.extractFromTask(this.dataItem, 'end');
if (isDevMode()) {
const taskTitle = this.mapper.extractFromTask(this.dataItem, 'title');
if (!taskStart || !taskEnd) {
console.warn(`Task ${taskTitle} is missing a start or end date.`);
}
if (taskStart && taskEnd && taskStart > taskEnd) {
console.warn(`Task ${taskTitle} has a start date that is after the end date.`);
}
}
}
ngAfterViewInit() {
this.taskDragService?.registerTask(this);
}
ngOnChanges(changes) {
if (isPresent(changes['dataItem'])) {
if (isPresent(changes['dataItem'].previousValue)) {
this.dependencyDomService.unregisterTask(changes['dataItem'].previousValue);
}
this.dependencyDomService.registerTask(this.dataItem, this.taskElement.nativeElement);
}
else if (isPresent(changes['activeView'])) {
this.dependencyDomService.notifyChanges();
}
if (this.navigationService.enabled && isPresent(changes['index'])) {
this.updateActiveState(this.navigationService.activeTask);
}
}
ngOnDestroy() {
if (isPresent(this.dataItem)) {
this.dependencyDomService.unregisterTask(this.dataItem);
}
this.subscriptions.unsubscribe();
}
updateActiveState({ activeIndex, isFocused }) {
const isActive = activeIndex === this.index;
const tabindex = isActive ? '0' : '-1';
this.taskElement.nativeElement.setAttribute('tabindex', tabindex);
if (isActive && isFocused) {
this.taskElement.nativeElement.focus();
this.taskElement.nativeElement.classList.add(FOCUSED_CLASS);
}
else {
this.taskElement.nativeElement.classList.remove(FOCUSED_CLASS);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GanttTaskBase, deps: [{ token: i1.MappingService }, { token: i2.TimelineViewService }, { token: i3.DependencyDomService }, { token: i4.OptionChangesService }, { token: i0.ChangeDetectorRef }, { token: i5.NavigationService }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: GanttTaskBase, selector: "kendo-gantt-task-base", inputs: { dataItem: "dataItem", index: "index", level: "level", renderDependencyDragClues: "renderDependencyDragClues", selectable: "selectable", isSelected: "isSelected", activeView: "activeView", taskClass: "taskClass" }, host: { properties: { "class.k-task-wrap": "this.wrapperClass", "attr.data-task-index": "this.taskIndexAttribute", "style.left.px": "this.taskOffset" } }, viewQueries: [{ propertyName: "taskElement", first: true, predicate: ["task"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GanttTaskBase, decorators: [{
type: Directive,
args: [{
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'kendo-gantt-task-base'
}]
}], ctorParameters: () => [{ type: i1.MappingService }, { type: i2.TimelineViewService }, { type: i3.DependencyDomService }, { type: i4.OptionChangesService }, { type: i0.ChangeDetectorRef }, { type: i5.NavigationService }], propDecorators: { wrapperClass: [{
type: HostBinding,
args: ['class.k-task-wrap']
}], taskIndexAttribute: [{
type: HostBinding,
args: ['attr.data-task-index']
}], taskElement: [{
type: ViewChild,
args: ['task', { static: true }]
}], dataItem: [{
type: Input
}], index: [{
type: Input
}], level: [{
type: Input
}], renderDependencyDragClues: [{
type: Input
}], selectable: [{
type: Input
}], isSelected: [{
type: Input
}], activeView: [{
type: Input
}], taskClass: [{
type: Input
}], taskOffset: [{
type: HostBinding,
args: ['style.left.px']
}] } });