devexpress-gantt
Version:
DevExpress Gantt Control
492 lines (465 loc) • 24.1 kB
text/typescript
import { Point } from "@devexpress/utils/lib/geometry/point";
import { GridElementInfo } from "../Helpers/GridElementInfo";
import { Task } from "../../Model/Entities/Task";
import { ViewType } from "../Helpers/Enums";
import { DateRange } from "../../Model/WorkingTime/DateRange";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { Browser } from "@devexpress/utils/lib/browser";
import { ViewVisualModelItem } from "../../Model/VisualModel/ViewVisualModelItem";
import { TaskEditTooltip } from "./TaskEditTooltip";
import { DependencyType } from "../../Model/Entities/Enums";
import { RenderHelper } from "../Render/RenderHelper";
import { TooltipSettings } from "../Settings/InitSettings/TooltipSettings";
import { Settings } from "../Settings/Settings";
import { ViewVisualModel } from "../../Model/VisualModel/VisualModel";
import { CommandManager } from "../../Commands/CommandManager";
import { TaskEditSettings } from "../Settings/InitSettings/TaskEditSettings";
import { ValidationController } from "../../Model/Validation/ValidationController";
import { DateUtils } from "../Utils/DateUtils";
import { UpdateTaskCommand } from "../../Commands/Task/UpdateTaskCommand";
export class TaskEditController {
private baseElement: HTMLElement;
private baseFrame: HTMLElement;
private progressEdit: HTMLElement;
private startEdit: HTMLElement;
private endEdit: HTMLElement;
private dependencyStart: HTMLElement;
private dependencyFinish: HTMLElement;
private timerId: number;
private showInfoDelay: number = 1000;
private dependencyLine: HTMLElement;
private dependencySuccessorBaseElement: HTMLElement;
private dependencySuccessorFrame: HTMLElement;
public dependencySuccessorStart: HTMLElement;
public dependencySuccessorFinish: HTMLElement;
private settings: TaskEditSettings;
private _tooltip: TaskEditTooltip;
private taskIndex: number = -1;
private successorIndex: number = -1;
private isEditingInProgress: boolean = false;
private disableTaskEditBox: boolean = false;
private isTaskEditBoxShown: boolean = false;
private startPosition: Point;
private taskDateRange: DateRange;
private onMouseLeaveHandler: any;
public dependencyId: string;
public wrapInfo: GridElementInfo;
private get taskId(): string {
return this.viewModel.items[this.taskIndex].task.internalId;
}
private get successorId(): string {
return this.viewModel.items[this.successorIndex].task.internalId;
}
private get task(): Task {
return this.viewItem.task;
}
private get viewItem(): ViewVisualModelItem {
return this.viewModel.items[this.taskIndex];
}
private get renderHelper(): RenderHelper {
return this.settings.getRenderHelper();
}
private get ganttSettings(): Settings {
return this.settings.getGanttSettings();
}
private get viewModel(): ViewVisualModel {
return this.settings.getViewModel();
}
private get commandManager(): CommandManager {
return this.settings.getCommandManager();
}
private get updateTaskCommand(): UpdateTaskCommand {
return this.commandManager.updateTaskCommand;
}
private get validationController(): ValidationController {
return this.settings.getValidationController();
}
private raiseTaskMoving(task: Task, newStart: Date, newEnd: Date, callback:(newStart, newEnd: Date) => void): boolean {
return this.settings.getModelManipulator().dispatcher.raiseTaskMoving(task, newStart, newEnd, callback);
}
public get tooltip(): TaskEditTooltip {
this._tooltip ??= new TaskEditTooltip(this.baseElement, this.tooltipSettings, this.renderHelper.elementTextHelperCultureInfo);
this._tooltip.tooltipSettings = this.tooltipSettings;
return this._tooltip;
}
public get tooltipSettings(): TooltipSettings {
const tooltipSettings = TooltipSettings.parse({
getHeaderHeight: this.settings.getRenderHelper().header.clientHeight,
getTaskProgressTooltipContentTemplate: this.ganttSettings.taskProgressTooltipContentTemplate,
getTaskTimeTooltipContentTemplate: this.ganttSettings.taskTimeTooltipContentTemplate,
getTaskTooltipContentTemplate: this.ganttSettings.taskTooltipContentTemplate,
destroyTemplate: (container: HTMLElement) => { this.settings.destroyTemplate(container); },
formatDate: (date: Date) => { return this.settings.formatDate(date); },
getTaskAreaContainer: () => { return this.settings.getRenderHelper().taskAreaContainer; }
});
return tooltipSettings;
}
constructor(settings: TaskEditSettings) {
this.settings = settings;
this.createElements();
}
public show(taskIndex: number): void {
if(this.isEditingInProgress || this.disableTaskEditBox)
return;
this.taskIndex = taskIndex;
this.hide();
this.changeWrapInfo();
this.baseElement.className = TaskEditController.CLASSNAMES.TASK_EDIT_BOX;
this.displayDependency();
if(this.task.isMilestone() && !this.viewItem.isCustom)
this.baseElement.className = this.baseElement.className + " milestone";
else {
if(!this.isTaskUpdateAllowed())
this.baseElement.className = this.baseElement.className + " " + TaskEditController.CLASSNAMES.TASK_EDIT_HIDE_UPDATING;
if(this.viewItem.isCustom)
this.baseElement.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_BOX_CUSTOM);
}
const delay = this.settings.getGanttSettings().editing.taskHoverDelay || 0;
this.taskDateRange = new DateRange(this.task.start, this.task.end);
this.displayTaskEditBox(delay);
this.displayProgressEdit();
this.displayStartEndEditElements();
}
private displayStartEndEditElements() {
const showElements = !this.task.isMilestone() && this.isTaskUpdateAllowed() && this.canUpdateTask();
if(!showElements) {
this.startEdit.style.display = "none";
this.endEdit.style.display = "none";
}
else {
this.startEdit.style.display = "block";
this.endEdit.style.display = "block";
}
}
private displayProgressEdit() {
if(!this.viewItem.isCustom && this.canUpdateTask() && this.isTaskUpdateAllowed() && this.wrapInfo.size.width > this.wrapInfo.size.height) {
this.progressEdit.style.display = "block";
this.progressEdit.style.left = ((this.task.normalizedProgress / 100) * this.wrapInfo.size.width - (this.progressEdit.offsetWidth / 2)) + "px";
}
else
this.progressEdit.style.display = "none";
}
private displayDependency() {
if(!this.ganttSettings.editing.enabled || !this.ganttSettings.editing.allowDependencyInsert || !this.ganttSettings.showDependencies)
this.baseElement.className = this.baseElement.className + " hide-dependency";
}
private changeWrapInfo() {
this.updateWrapInfo();
this.wrapInfo.assignPosition(this.baseElement);
this.wrapInfo.assignSize(this.baseElement);
}
private displayTaskEditBox(delay: number = 0) {
const showFunc = () => {
this.renderHelper.taskArea.appendChild(this.baseElement);
this.isTaskEditBoxShown = true;
};
if(delay)
this.timerId = setTimeout(showFunc, delay);
else
showFunc();
}
public endEditing(): void {
this.isEditingInProgress = false;
this.hide();
}
public hide(): void {
this.isTaskEditBoxShown = false;
const parentNode: Node = this.baseElement.parentNode;
if(parentNode)
parentNode.removeChild(this.baseElement);
this.tooltip.hide();
clearTimeout(this.timerId);
}
public cancel(): void {
clearTimeout(this.timerId);
}
public showTaskInfo(posX: number, delay: number = 500): void {
if(this.timerId)
delay = this.showInfoDelay;
this.tooltip.showInfo(this.task, posX, delay);
}
private updateWrapInfo(): void {
this.wrapInfo = this.getTaskWrapperElementInfo(this.taskIndex);
this.wrapInfo.position.x--;
}
private isAllowedToConnectTasks(taskIndex: number) {
const successorViewItem = this.viewModel.items[taskIndex];
return this.validationController.canCreateDependency(this.taskId, successorViewItem.task?.internalId);
}
public showDependencySuccessor(taskIndex: number): void {
if(this.isAllowedToConnectTasks(taskIndex)) {
this.successorIndex = taskIndex;
const wrapInfo = this.getTaskWrapperElementInfo(taskIndex);
wrapInfo.assignPosition(this.dependencySuccessorBaseElement);
wrapInfo.assignSize(this.dependencySuccessorBaseElement);
wrapInfo.assignSize(this.dependencySuccessorFrame);
this.renderHelper.taskArea.appendChild(this.dependencySuccessorBaseElement);
}
}
public hideDependencySuccessor(): void {
const parentNode: Node = this.dependencySuccessorBaseElement.parentNode;
if(parentNode)
parentNode.removeChild(this.dependencySuccessorBaseElement);
this.successorIndex = -1;
}
public processProgress(position: Point): void {
if(!this.isTaskUpdateAllowed())
return;
this.isEditingInProgress = true;
const progressOffset = position.x - this.wrapInfo.position.x;
let progress = 0;
if(position.x > this.wrapInfo.position.x)
if(position.x < this.wrapInfo.position.x + this.wrapInfo.size.width)
progress = Math.round((progressOffset) / this.baseElement.clientWidth * 100);
else
progress = 100;
this.progressEdit.style.left = ((progress / 100) *
this.wrapInfo.size.width - (this.progressEdit.offsetWidth / 2)) + "px";
this.tooltip.showProgress(progress,
DomUtils.getAbsolutePositionX(this.progressEdit) + this.progressEdit.offsetWidth / 2);
}
public confirmProgress(): void {
if(this.isTaskUpdateAllowed()) {
this.isEditingInProgress = false;
const progress = Math.round((this.progressEdit.offsetLeft + (this.progressEdit.offsetWidth / 2)) / this.wrapInfo.size.width * 100);
this.updateTaskCommand.execute(this.taskId, { progress: progress });
}
}
public processEnd(position: Point): void {
if(!this.isTaskUpdateAllowed())
return;
this.baseElement.className = this.baseElement.className + " move";
this.isEditingInProgress = true;
const positionX = position.x > this.wrapInfo.position.x ? position.x : this.wrapInfo.position.x;
const width = positionX - this.wrapInfo.position.x;
this.baseElement.style.width = (width < 1 ? 0 : width) + "px";
const startDate = this.task.start;
const date = this.renderHelper.gridLayoutCalculator.getDateByPos(positionX);
date.setSeconds(0);
if(date < startDate || width < 1)
this.taskDateRange.end.setTime(startDate.getTime());
else
this.taskDateRange.end = this.getCorrectedDate(this.task.end, date);
this.tooltip.showTime(startDate, this.taskDateRange.end, DomUtils.getAbsolutePositionX(this.baseElement) + this.baseElement.clientWidth);
}
public confirmEnd(): void {
if(this.isTaskUpdateAllowed()) {
this.baseElement.className = TaskEditController.CLASSNAMES.TASK_EDIT_BOX;
this.isEditingInProgress = false;
this.updateTaskCommand.execute(this.taskId, { end: this.taskDateRange.end });
this.hide();
this.updateWrapInfo();
}
}
public processStart(position: Point): void {
if(!this.isTaskUpdateAllowed())
return;
this.baseElement.className = this.baseElement.className + " move";
this.isEditingInProgress = true;
const positionX = position.x < this.wrapInfo.position.x + this.wrapInfo.size.width ? position.x : this.wrapInfo.position.x + this.wrapInfo.size.width;
const width = this.wrapInfo.size.width - (positionX - this.wrapInfo.position.x);
this.baseElement.style.left = positionX + "px";
this.baseElement.style.width = (width < 1 ? 0 : width) + "px";
const endDate = this.task.end;
const date = this.renderHelper.gridLayoutCalculator.getDateByPos(positionX);
date.setSeconds(0);
if(date > endDate || width < 1)
this.taskDateRange.start.setTime(endDate.getTime());
else
this.taskDateRange.start = this.getCorrectedDate(this.task.start, date);
this.tooltip.showTime(this.taskDateRange.start, endDate, DomUtils.getAbsolutePositionX(this.baseElement));
}
public confirmStart(): void {
if(this.isTaskUpdateAllowed()) {
this.baseElement.className = TaskEditController.CLASSNAMES.TASK_EDIT_BOX;
this.isEditingInProgress = false;
this.updateTaskCommand.execute(this.taskId, { start: this.taskDateRange.start });
this.hide();
this.updateWrapInfo();
}
}
public processMove(delta: number): boolean {
if(this.isTaskUpdateAllowed() && this.isTaskEditBoxShown) {
this.baseElement.className = this.baseElement.className + " move";
const left = this.baseElement.offsetLeft + delta;
this.baseElement.style.left = left + "px";
const startDate = this.renderHelper.gridLayoutCalculator.getDateByPos(left);
this.taskDateRange.start = this.getCorrectedDate(this.task.start, startDate);
const taskPeriod = DateUtils.getRangeMSPeriod(this.task.start, this.task.end);
this.taskDateRange.end = DateUtils.getDSTCorrectedTaskEnd(this.taskDateRange.start, taskPeriod);
this.isEditingInProgress = this.raiseTaskMoving(this.task, this.taskDateRange.start, this.taskDateRange.end, this.onTaskMovingCallback.bind(this));
if(this.isEditingInProgress)
this.tooltip.showTime(this.taskDateRange.start, this.taskDateRange.end, DomUtils.getAbsolutePositionX(this.baseElement));
return this.isEditingInProgress;
}
return true;
}
onTaskMovingCallback(newStart: Date, newEnd: Date): void {
if(this.taskDateRange.start === newStart && this.taskDateRange.end === newEnd)
return;
const calculator = this.renderHelper.gridLayoutCalculator;
const newLeft = calculator.getPosByDate(newStart);
const newWidth = calculator.getPosByDate(newEnd) - newLeft;
this.baseElement.style.left = newLeft + "px";
this.baseElement.style.width = (newWidth < 1 ? 0 : newWidth) + "px";
this.taskDateRange.start = newStart;
this.taskDateRange.end = newEnd;
}
public confirmMove(): void {
if(this.isTaskUpdateAllowed()) {
if(!this.ganttSettings.editing.allowDependencyInsert)
this.baseElement.className = this.baseElement.className + " hide-dependency";
if(this.isEditingInProgress) {
this.baseElement.className = TaskEditController.CLASSNAMES.TASK_EDIT_BOX;
this.updateTaskCommand.execute(this.taskId, { start: this.taskDateRange.start, end: this.taskDateRange.end });
this.updateWrapInfo();
this.hide();
this.isEditingInProgress = false;
}
}
}
private getCorrectedDate(referenceDate: Date, newDate: Date): Date {
if(this.ganttSettings.viewType > ViewType.SixHours) {
const year = newDate.getFullYear();
const month = newDate.getMonth();
const day = newDate.getDate();
const hours = this.ganttSettings.viewType === ViewType.Days ? newDate.getHours() : referenceDate.getHours();
const minutes = referenceDate.getMinutes();
const sec = referenceDate.getSeconds();
const msec = referenceDate.getMilliseconds();
return new Date(year, month, day, hours, minutes, sec, msec);
}
return newDate;
}
public startDependency(pos: Point): void {
this.dependencyLine = document.createElement("DIV");
this.dependencyLine.className = TaskEditController.CLASSNAMES.TASK_EDIT_DEPENDENCY_LINE;
this.renderHelper.taskArea.appendChild(this.dependencyLine);
this.startPosition = pos;
}
public processDependency(pos: Point): void {
this.isEditingInProgress = true;
this.drawline(this.startPosition, pos);
}
public endDependency(type: DependencyType): void {
this.isEditingInProgress = false;
if(type != null)
this.commandManager.createDependencyCommand.execute(this.task.internalId, this.successorId, type);
const parentNode: Node = this.dependencyLine.parentNode;
if(parentNode)
parentNode.removeChild(this.dependencyLine);
this.dependencyLine = null;
this.hideDependencySuccessor();
this.hide();
}
public selectDependency(id: string): void {
if(this.ganttSettings.editing.allowDependencyDelete)
this.dependencyId = id;
}
public isDependencySelected(id: string): boolean {
return this.dependencyId && this.dependencyId === id;
}
public deleteSelectedDependency(): void {
if(this.dependencyId)
this.commandManager.removeDependencyCommand.execute(this.dependencyId);
}
private getTaskWrapperElementInfo(taskIndex: number): GridElementInfo {
const calculator = this.renderHelper.gridLayoutCalculator;
const info = calculator.getTaskWrapperElementInfo(taskIndex);
info.size.width = calculator.getTaskWidth(taskIndex);
info.size.height = calculator.getTaskHeight(taskIndex);
return info;
}
public createElements(): void {
this.baseElement = document.createElement("DIV");
this.baseFrame = document.createElement("DIV");
this.baseFrame.className = TaskEditController.CLASSNAMES.TASK_EDIT_FRAME;
this.baseElement.appendChild(this.baseFrame);
this.progressEdit = document.createElement("DIV");
this.progressEdit.className = TaskEditController.CLASSNAMES.TASK_EDIT_PROGRESS;
this.baseFrame.appendChild(this.progressEdit);
this.progressEdit.appendChild(document.createElement("DIV"));
this.dependencyFinish = document.createElement("DIV");
this.dependencyFinish.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_DEPENDENCY_RIGTH);
if(Browser.TouchUI)
this.dependencyFinish.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_TOUCH);
this.baseFrame.appendChild(this.dependencyFinish);
this.dependencyStart = document.createElement("DIV");
this.dependencyStart.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_DEPENDENCY_LEFT);
if(Browser.TouchUI)
this.dependencyStart.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_TOUCH);
this.baseFrame.appendChild(this.dependencyStart);
this.startEdit = document.createElement("DIV");
this.startEdit.className = TaskEditController.CLASSNAMES.TASK_EDIT_START;
this.baseFrame.appendChild(this.startEdit);
this.endEdit = document.createElement("DIV");
this.endEdit.className = TaskEditController.CLASSNAMES.TASK_EDIT_END;
this.baseFrame.appendChild(this.endEdit);
this.dependencySuccessorBaseElement = document.createElement("DIV");
this.dependencySuccessorBaseElement.className = TaskEditController.CLASSNAMES.TASK_EDIT_BOX_SUCCESSOR;
this.dependencySuccessorFrame = document.createElement("DIV");
this.dependencySuccessorFrame.className = TaskEditController.CLASSNAMES.TASK_EDIT_FRAME_SUCCESSOR;
this.dependencySuccessorBaseElement.appendChild(this.dependencySuccessorFrame);
this.dependencySuccessorStart = document.createElement("DIV");
this.dependencySuccessorStart.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_SUCCESSOR_DEPENDENCY_RIGTH);
if(Browser.TouchUI)
this.dependencySuccessorStart.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_TOUCH);
this.dependencySuccessorFrame.appendChild(this.dependencySuccessorStart);
this.dependencySuccessorFinish = document.createElement("DIV");
this.dependencySuccessorFinish.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_SUCCESSOR_DEPENDENCY_LEFT);
if(Browser.TouchUI)
this.dependencySuccessorFinish.classList.add(TaskEditController.CLASSNAMES.TASK_EDIT_TOUCH);
this.dependencySuccessorFrame.appendChild(this.dependencySuccessorFinish);
this._tooltip = new TaskEditTooltip(this.baseElement, this.tooltipSettings, this.renderHelper.elementTextHelperCultureInfo);
this.attachEvents();
}
private attachEvents(): void {
this.onMouseLeaveHandler = function() {
if(!this.isEditingInProgress)
this.hide();
}.bind(this);
this.baseElement.addEventListener("mouseleave", this.onMouseLeaveHandler);
}
private drawline(start: Point, end: Point): void {
if(start.x > end.x) {
const temp = end;
end = start;
start = temp;
}
let angle = Math.atan((start.y - end.y) / (end.x - start.x));
angle = (angle * 180 / Math.PI);
angle = -angle;
const length = Math.sqrt((start.x - end.x) * (start.x - end.x) + (start.y - end.y) * (start.y - end.y));
this.dependencyLine.style.left = start.x + "px";
this.dependencyLine.style.top = start.y + "px";
this.dependencyLine.style.width = length + "px";
this.dependencyLine.style.transform = "rotate(" + angle + "deg)";
}
private canUpdateTask(): boolean {
return !this.viewModel.isTaskToCalculateByChildren(this.task.internalId);
}
private isTaskUpdateAllowed(): boolean {
const settings = this.ganttSettings.editing;
return settings.enabled && settings.allowTaskUpdate;
}
public detachEvents(): void {
this.baseElement?.removeEventListener("mouseleave", this.onMouseLeaveHandler);
}
public static CLASSNAMES = {
TASK_EDIT_BOX: "dx-gantt-task-edit-wrapper",
TASK_EDIT_BOX_CUSTOM: "dx-gantt-task-edit-wrapper-custom",
TASK_EDIT_FRAME: "dx-gantt-task-edit-frame",
TASK_EDIT_PROGRESS: "dx-gantt-task-edit-progress",
TASK_EDIT_DEPENDENCY_RIGTH: "dx-gantt-task-edit-dependency-r",
TASK_EDIT_DEPENDENCY_LEFT: "dx-gantt-task-edit-dependency-l",
TASK_EDIT_START: "dx-gantt-task-edit-start",
TASK_EDIT_END: "dx-gantt-task-edit-end",
TASK_EDIT_DEPENDENCY_LINE: "dx-gantt-task-edit-dependency-line",
TASK_EDIT_BOX_SUCCESSOR: "dx-gantt-task-edit-wrapper-successor",
TASK_EDIT_FRAME_SUCCESSOR: "dx-gantt-task-edit-frame-successor",
TASK_EDIT_SUCCESSOR_DEPENDENCY_RIGTH: "dx-gantt-task-edit-successor-dependency-r",
TASK_EDIT_SUCCESSOR_DEPENDENCY_LEFT: "dx-gantt-task-edit-successor-dependency-l",
TASK_EDIT_TOUCH: "dx-gantt-edit-touch",
TASK_EDIT_HIDE_UPDATING: "hide-updating"
};
}