@progress/kendo-angular-gantt
Version:
Kendo UI Angular Gantt
240 lines (239 loc) • 11.4 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 { Injectable, NgZone } from '@angular/core';
import { Subject } from 'rxjs';
import { MappingService } from '../common/mapping.service';
import { isPresent } from '@progress/kendo-angular-common';
import * as i0 from "@angular/core";
import * as i1 from "../common/mapping.service";
/**
* @hidden
*/
export class TaskDragService {
mapper;
zone;
taskDraggable;
drag = new Subject();
dragEnd = new Subject();
dragStart = new Subject();
tasksContainer;
tasksContainerRect;
scrollableContainer;
currentDragTask;
currentDragTaskRect;
leftDragHandle;
rightDragHandle;
completionRatioHandle;
tasks = [];
editedProp;
prevX;
prevWidth;
prevLeft;
prevScrollLeft;
newRange;
offsetX;
maxWidth;
newCompletionRatio;
constructor(mapper, zone) {
this.mapper = mapper;
this.zone = zone;
}
registerTask(task) {
if (!this.tasks.some(t => t === task)) {
this.tasks.push(task);
}
}
onDragStart(args) {
this.tasksContainerRect = this.tasksContainer.getBoundingClientRect();
this.currentDragTask = this.tasks.find(t => t.taskElement.nativeElement.contains(args.dragTarget) || t.taskElement.nativeElement.nextElementSibling === args.dragTarget);
const taskEl = this.currentDragTask.taskElement.nativeElement;
this.currentDragTaskRect = taskEl.getBoundingClientRect();
this.completionRatioHandle = args.dragTarget.classList.contains('k-task-draghandle');
this.leftDragHandle = args.dragTarget.classList.contains('k-resize-w');
this.rightDragHandle = args.dragTarget.classList.contains('k-resize-e');
if (this.leftDragHandle) {
this.editedProp = 'start';
}
else if (this.rightDragHandle) {
this.editedProp = 'end';
}
else {
this.editedProp = 'both';
}
this.dragStart.next(args);
}
onDrag(args) {
if (this.completionRatioHandle) { // completion ratio handle drag
const taskWidth = this.currentDragTaskRect.width;
const scrollLeft = this.scrollableContainer?.scrollLeft;
const scrollDelta = isPresent(this.prevScrollLeft) ? this.prevScrollLeft - scrollLeft : 0;
const completionWidth = Math.min(Math.max(args.dragEvent.clientX - this.currentDragTaskRect.left - scrollDelta, 0), taskWidth);
const newCompletionRatio = completionWidth / taskWidth;
this.currentDragTask.draggedCompletionWidth = completionWidth;
!this.prevScrollLeft && (this.prevScrollLeft = scrollLeft);
this.newCompletionRatio = newCompletionRatio;
this.drag.next({
start: this.mapper.extractFromTask(this.currentDragTask.dataItem, 'start'),
end: this.mapper.extractFromTask(this.currentDragTask.dataItem, 'end'),
completionRatio: newCompletionRatio,
dragEvent: args,
item: this.currentDragTask.dataItem,
offset: this.prevX,
width: this.prevWidth
});
}
else { // task resize or drag
const marquee = this.calculateMarquee(args, this.leftDragHandle, this.rightDragHandle);
if (!marquee) {
return;
}
const { left, width } = marquee;
this.newRange = this.calculateStartEnd(this.editedProp, width, left);
this.drag.next({
start: this.newRange.start,
end: this.newRange.end,
completionRatio: this.mapper.extractFromTask(this.currentDragTask.dataItem, 'completionRatio'),
dragEvent: args,
item: this.currentDragTask.dataItem,
offset: left,
width
});
}
}
onDragEnd(args) {
if (this.completionRatioHandle) {
this.dragEnd.next({
start: this.mapper.extractFromTask(this.currentDragTask.dataItem, 'start'),
end: this.mapper.extractFromTask(this.currentDragTask.dataItem, 'end'),
completionRatio: this.newCompletionRatio,
dragEvent: args,
item: this.currentDragTask.dataItem,
offset: this.prevX,
width: this.prevWidth
});
}
else {
this.dragEnd.next({
start: this.newRange.start,
end: this.newRange.end,
completionRatio: this.mapper.extractFromTask(this.currentDragTask.dataItem, 'completionRatio'),
dragEvent: args,
item: this.currentDragTask.dataItem,
offset: this.prevX,
width: this.prevWidth
});
}
this.prevWidth = this.prevX = this.prevLeft = this.prevScrollLeft = this.offsetX = this.maxWidth = this.newCompletionRatio = null;
this.currentDragTaskRect = this.currentDragTask = this.currentDragTask.draggedCompletionWidth = this.editedProp = this.newRange = null;
this.leftDragHandle = this.rightDragHandle = this.completionRatioHandle = false;
}
calculateMarquee(e, leftDragHandle, rightDragHandle) {
const clientX = e.dragEvent.clientX;
// Update container rect when dragging outside of it to avoid miscalculations
// coming from scrolling the whole page during dragging a task
if (clientX < this.tasksContainerRect.x || clientX > this.tasksContainerRect.right) {
this.zone.runOutsideAngular(() => {
this.tasksContainerRect = this.tasksContainer.getBoundingClientRect();
this.currentDragTaskRect = this.currentDragTask.taskElement.nativeElement.getBoundingClientRect();
});
return;
}
const scrollLeft = this.scrollableContainer?.scrollLeft;
const scrollDelta = isPresent(this.prevScrollLeft) ? this.prevScrollLeft - scrollLeft : 0;
let left, width;
if (leftDragHandle) {
if (!isPresent(this.maxWidth)) {
this.maxWidth = this.currentDragTaskRect.right - this.tasksContainerRect.x + scrollLeft;
}
left = Math.min(Math.max(clientX - this.tasksContainerRect.x - scrollDelta, 0), this.maxWidth);
width = Math.min(Math.max(this.currentDragTaskRect.right - clientX + scrollDelta, 0), this.maxWidth);
}
else if (rightDragHandle) {
if (!isPresent(this.maxWidth)) {
this.maxWidth = this.tasksContainerRect.right - this.currentDragTaskRect.x;
}
left = this.prevLeft ?? (this.currentDragTaskRect.x - this.tasksContainerRect.x);
width = Math.min(Math.max(clientX - this.tasksContainerRect.x - left - scrollDelta, 0), this.maxWidth);
}
else { // dragging the whole task
if (!isPresent(this.offsetX)) {
this.offsetX = e.dragEvent.offsetX;
}
left = Math.max(clientX - this.tasksContainerRect.x - this.offsetX - scrollDelta, 0);
}
const cursorLeft = clientX - this.tasksContainerRect.x;
// out of limits (0, max possible width) when dragging the left handle
if (this.leftDragHandle && (cursorLeft < 0 || this.maxWidth < cursorLeft)) {
return {
left: cursorLeft < 0 ? 0 : this.maxWidth,
width
};
}
// out of limits (0, max possible width) when dragging right handle
if (rightDragHandle && (clientX < this.currentDragTaskRect.x) || clientX > this.tasksContainerRect.right) {
return {
left,
width: clientX < this.currentDragTaskRect.x ? 0 : this.maxWidth
};
}
// out of limits (start < view start || end > view end) when dragging the whole task
if (!(this.leftDragHandle || this.rightDragHandle) && (left < 0 || (clientX + this.currentDragTaskRect.width - scrollDelta - this.offsetX > this.tasksContainerRect.right))) {
return {
left: Math.min(Math.max(left, 0), this.tasksContainerRect.width - this.currentDragTaskRect.width),
width: this.currentDragTaskRect.width
};
}
this.prevX = clientX;
this.prevLeft = left;
this.prevWidth = width;
!this.prevScrollLeft && (this.prevScrollLeft = scrollLeft);
return {
left,
width: width ?? this.currentDragTaskRect.width
};
}
calculateStartEnd(editedProp, width, left) {
const startDate = this.mapper.extractFromTask(this.currentDragTask.dataItem, 'start');
const endDate = this.mapper.extractFromTask(this.currentDragTask.dataItem, 'end');
const duration = endDate - startDate;
const delta = left - this.currentDragTaskRect.x + this.tasksContainerRect.x;
if (editedProp === 'both') {
if (duration) {
const delta = left - this.currentDragTaskRect.x + this.tasksContainerRect.left;
const coef = delta / this.currentDragTask.taskWidth;
const deltaMs = duration * coef;
const newStart = new Date(startDate.getTime() + deltaMs);
const newEnd = new Date(newStart.getTime() + duration);
return {
start: newStart,
end: newEnd
};
}
else { // milestone task
const initialOffset = this.currentDragTaskRect.x - this.tasksContainerRect.x;
const msPerPx = (startDate - this.currentDragTask.viewService.viewStart) / initialOffset;
const deltaMs = delta * msPerPx;
return {
start: new Date(startDate.getTime() + deltaMs),
end: new Date(startDate.getTime() + deltaMs)
};
}
}
const coef = width / this.currentDragTask.taskWidth;
const newDuration = duration * coef;
const newPropDate = editedProp === 'start' ? new Date(endDate - newDuration) : new Date(startDate.getTime() + newDuration);
const newStart = editedProp === 'start' ? newPropDate : startDate;
const newEnd = editedProp === 'end' ? newPropDate : endDate;
return {
start: newStart,
end: newEnd
};
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TaskDragService, deps: [{ token: i1.MappingService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TaskDragService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TaskDragService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.MappingService }, { type: i0.NgZone }] });