UNPKG

devexpress-gantt

Version:

DevExpress Gantt Control

492 lines (465 loc) 24.1 kB
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" }; }