UNPKG

@progress/kendo-angular-gantt

Version:
224 lines (223 loc) 11.1 kB
/**----------------------------------------------------------------------------------------- * 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'] }] } });