500 lines
14 KiB
TypeScript
Raw Normal View History

2022-02-13 20:35:30 +01:00
import React, {
useState,
SyntheticEvent,
useRef,
useEffect,
useMemo,
} from "react";
2021-08-09 22:21:18 +03:00
import { ViewMode, GanttProps, Task } from "../../types/public-types";
import { GridProps } from "../grid/grid";
2020-08-05 08:14:22 +03:00
import { ganttDateRange, seedDates } from "../../helpers/date-helper";
import { CalendarProps } from "../calendar/calendar";
import { TaskGanttContentProps } from "./task-gantt-content";
import { TaskListHeaderDefault } from "../task-list/task-list-header";
import { TaskListTableDefault } from "../task-list/task-list-table";
import { StandardTooltipContent, Tooltip } from "../other/tooltip";
import { VerticalScroll } from "../other/vertical-scroll";
import { TaskListProps, TaskList } from "../task-list/task-list";
import { TaskGantt } from "./task-gantt";
2021-03-01 21:55:27 +02:00
import { BarTask } from "../../types/bar-task";
import { convertToBarTasks } from "../../helpers/bar-helper";
import { GanttEvent } from "../../types/gantt-task-actions";
2021-03-21 16:58:20 +02:00
import { DateSetup } from "../../types/date-setup";
2021-03-25 23:06:20 +02:00
import styles from "./gantt.module.css";
import { HorizontalScroll } from "../other/horizontal-scroll";
2022-02-13 20:35:30 +01:00
import { removeHiddenTasks, sortTasks } from "../../helpers/other-helper";
2020-07-22 20:50:43 +03:00
2021-03-01 21:55:27 +02:00
export const Gantt: React.FunctionComponent<GanttProps> = ({
2020-07-22 20:50:43 +03:00
tasks,
headerHeight = 50,
columnWidth = 60,
2020-09-01 23:08:15 +03:00
listCellWidth = "155px",
2020-07-22 20:50:43 +03:00
rowHeight = 50,
ganttHeight = 0,
2020-07-22 20:50:43 +03:00
viewMode = ViewMode.Day,
2020-08-05 08:14:22 +03:00
locale = "en-GB",
2020-07-22 20:50:43 +03:00
barFill = 60,
barCornerRadius = 3,
2020-08-05 08:14:22 +03:00
barProgressColor = "#a3a3ff",
barProgressSelectedColor = "#8282f5",
barBackgroundColor = "#b8c2cc",
barBackgroundSelectedColor = "#aeb8c2",
2021-03-29 17:36:53 +03:00
projectProgressColor = "#7db59a",
projectProgressSelectedColor = "#59a985",
projectBackgroundColor = "#fac465",
projectBackgroundSelectedColor = "#f7bb53",
2021-03-01 21:55:27 +02:00
milestoneBackgroundColor = "#f1c453",
milestoneBackgroundSelectedColor = "#f29e4c",
2021-06-28 23:15:53 +03:00
rtl = false,
2020-07-22 20:50:43 +03:00
handleWidth = 8,
timeStep = 300000,
2020-08-05 08:14:22 +03:00
arrowColor = "grey",
fontFamily = "Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue",
fontSize = "14px",
2020-07-22 20:50:43 +03:00
arrowIndent = 20,
2020-08-05 08:14:22 +03:00
todayColor = "rgba(252, 248, 227, 0.5)",
2022-02-06 21:52:49 +01:00
viewDate,
TooltipContent = StandardTooltipContent,
TaskListHeader = TaskListHeaderDefault,
TaskListTable = TaskListTableDefault,
2020-07-22 20:50:43 +03:00
onDateChange,
onProgressChange,
onDoubleClick,
2022-02-22 16:35:56 +01:00
onClick,
2021-03-27 21:05:38 +02:00
onDelete,
onSelect,
2021-08-09 22:21:18 +03:00
onExpanderClick,
2020-07-22 20:50:43 +03:00
}) => {
2020-08-29 14:01:04 +03:00
const wrapperRef = useRef<HTMLDivElement>(null);
const taskListRef = useRef<HTMLDivElement>(null);
2021-03-21 16:58:20 +02:00
const [dateSetup, setDateSetup] = useState<DateSetup>(() => {
2021-03-01 21:55:27 +02:00
const [startDate, endDate] = ganttDateRange(tasks, viewMode);
2021-03-21 16:58:20 +02:00
return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
2021-03-01 21:55:27 +02:00
});
2022-02-06 21:52:49 +01:00
const [currentViewDate, setCurrentViewDate] = useState<Date | undefined>(
undefined
);
2021-03-01 21:55:27 +02:00
const [taskListWidth, setTaskListWidth] = useState(0);
const [svgContainerWidth, setSvgContainerWidth] = useState(0);
const [svgContainerHeight, setSvgContainerHeight] = useState(ganttHeight);
2021-03-01 21:55:27 +02:00
const [barTasks, setBarTasks] = useState<BarTask[]>([]);
const [ganttEvent, setGanttEvent] = useState<GanttEvent>({
action: "",
});
2022-02-13 20:35:30 +01:00
const taskHeight = useMemo(
() => (rowHeight * barFill) / 100,
[rowHeight, barFill]
);
2021-03-01 21:55:27 +02:00
const [selectedTask, setSelectedTask] = useState<BarTask>();
const [failedTask, setFailedTask] = useState<BarTask | null>(null);
2021-03-25 23:06:20 +02:00
const svgWidth = dateSetup.dates.length * columnWidth;
2021-03-01 21:55:27 +02:00
const ganttFullHeight = barTasks.length * rowHeight;
2021-08-03 21:27:44 +03:00
const [scrollY, setScrollY] = useState(0);
const [scrollX, setScrollX] = useState(-1);
const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false);
2021-03-01 21:55:27 +02:00
// task change events
2020-08-31 23:26:44 +03:00
useEffect(() => {
2021-08-11 11:28:19 +03:00
let filteredTasks: Task[];
if (onExpanderClick) {
filteredTasks = removeHiddenTasks(tasks);
} else {
filteredTasks = tasks;
}
2022-02-13 20:35:30 +01:00
filteredTasks = filteredTasks.sort(sortTasks);
2021-08-09 22:21:18 +03:00
const [startDate, endDate] = ganttDateRange(filteredTasks, viewMode);
2021-06-28 23:15:53 +03:00
let newDates = seedDates(startDate, endDate, viewMode);
if (rtl) {
newDates = newDates.reverse();
2021-08-03 21:27:44 +03:00
if (scrollX === -1) {
setScrollX(newDates.length * columnWidth);
}
2021-06-28 23:15:53 +03:00
}
2021-03-21 16:58:20 +02:00
setDateSetup({ dates: newDates, viewMode });
2021-03-01 21:55:27 +02:00
setBarTasks(
convertToBarTasks(
2021-08-09 22:21:18 +03:00
filteredTasks,
2021-03-01 21:55:27 +02:00
newDates,
columnWidth,
rowHeight,
taskHeight,
barCornerRadius,
handleWidth,
2021-06-28 23:15:53 +03:00
rtl,
2021-03-01 21:55:27 +02:00
barProgressColor,
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor,
2021-03-28 16:12:28 +03:00
projectProgressColor,
projectProgressSelectedColor,
projectBackgroundColor,
projectBackgroundSelectedColor,
2021-03-01 21:55:27 +02:00
milestoneBackgroundColor,
milestoneBackgroundSelectedColor
)
);
}, [
tasks,
viewMode,
rowHeight,
barCornerRadius,
columnWidth,
taskHeight,
handleWidth,
barProgressColor,
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor,
2021-03-28 16:12:28 +03:00
projectProgressColor,
projectProgressSelectedColor,
projectBackgroundColor,
projectBackgroundSelectedColor,
milestoneBackgroundColor,
milestoneBackgroundSelectedColor,
2021-06-28 23:15:53 +03:00
rtl,
2021-08-03 21:27:44 +03:00
scrollX,
2021-08-11 11:28:19 +03:00
onExpanderClick,
2021-03-01 21:55:27 +02:00
]);
2022-02-06 21:52:49 +01:00
useEffect(() => {
if (
viewMode === dateSetup.viewMode &&
((viewDate && !currentViewDate) ||
(viewDate && currentViewDate?.valueOf() !== viewDate.valueOf()))
) {
const dates = dateSetup.dates;
const index = dates.findIndex(
(d, i) =>
viewDate.valueOf() >= d.valueOf() &&
i + 1 !== dates.length &&
viewDate.valueOf() < dates[i + 1].valueOf()
);
if (index === -1) {
return;
}
setCurrentViewDate(viewDate);
setScrollX(columnWidth * index);
}
}, [
viewDate,
columnWidth,
dateSetup.dates,
2022-05-29 10:56:25 +02:00
dateSetup.viewMode,
2022-02-06 21:52:49 +01:00
viewMode,
currentViewDate,
setCurrentViewDate,
]);
2021-03-01 21:55:27 +02:00
useEffect(() => {
const { changedTask, action } = ganttEvent;
if (changedTask) {
if (action === "delete") {
setGanttEvent({ action: "" });
setBarTasks(barTasks.filter(t => t.id !== changedTask.id));
} else if (
action === "move" ||
action === "end" ||
action === "start" ||
action === "progress"
) {
const prevStateTask = barTasks.find(t => t.id === changedTask.id);
if (
prevStateTask &&
(prevStateTask.start.getTime() !== changedTask.start.getTime() ||
prevStateTask.end.getTime() !== changedTask.end.getTime() ||
prevStateTask.progress !== changedTask.progress)
) {
// actions for change
const newTaskList = barTasks.map(t =>
t.id === changedTask.id ? changedTask : t
);
setBarTasks(newTaskList);
}
}
}
}, [ganttEvent, barTasks]);
useEffect(() => {
if (failedTask) {
setBarTasks(barTasks.map(t => (t.id !== failedTask.id ? t : failedTask)));
setFailedTask(null);
}
}, [failedTask, barTasks]);
useEffect(() => {
if (!listCellWidth) {
setTaskListWidth(0);
}
if (taskListRef.current) {
setTaskListWidth(taskListRef.current.offsetWidth);
}
}, [taskListRef, listCellWidth]);
useEffect(() => {
if (wrapperRef.current) {
setSvgContainerWidth(wrapperRef.current.offsetWidth - taskListWidth);
}
}, [wrapperRef, taskListWidth]);
useEffect(() => {
if (ganttHeight) {
setSvgContainerHeight(ganttHeight + headerHeight);
} else {
setSvgContainerHeight(tasks.length * rowHeight + headerHeight);
}
2022-05-29 10:56:25 +02:00
}, [ganttHeight, tasks, headerHeight, rowHeight]);
2020-08-29 14:01:04 +03:00
// scroll events
useEffect(() => {
const handleWheel = (event: WheelEvent) => {
if (event.shiftKey || event.deltaX) {
const scrollMove = event.deltaX ? event.deltaX : event.deltaY;
let newScrollX = scrollX + scrollMove;
if (newScrollX < 0) {
newScrollX = 0;
} else if (newScrollX > svgWidth) {
newScrollX = svgWidth;
}
setScrollX(newScrollX);
event.preventDefault();
2021-08-03 21:27:44 +03:00
} else if (ganttHeight) {
let newScrollY = scrollY + event.deltaY;
if (newScrollY < 0) {
newScrollY = 0;
} else if (newScrollY > ganttFullHeight - ganttHeight) {
newScrollY = ganttFullHeight - ganttHeight;
}
if (newScrollY !== scrollY) {
setScrollY(newScrollY);
event.preventDefault();
}
2020-08-29 14:01:04 +03:00
}
2020-08-29 14:01:04 +03:00
setIgnoreScrollEvent(true);
};
2020-07-22 20:50:43 +03:00
2020-08-29 14:01:04 +03:00
// subscribe if scroll is necessary
2022-06-28 20:20:37 +02:00
wrapperRef.current?.addEventListener("wheel", handleWheel, {
passive: false,
});
2020-08-29 14:01:04 +03:00
return () => {
2022-06-28 20:20:37 +02:00
wrapperRef.current?.removeEventListener("wheel", handleWheel);
2020-08-29 14:01:04 +03:00
};
2022-05-29 10:56:25 +02:00
}, [
wrapperRef,
scrollY,
scrollX,
ganttHeight,
svgWidth,
rtl,
ganttFullHeight,
]);
2020-08-29 14:01:04 +03:00
const handleScrollY = (event: SyntheticEvent<HTMLDivElement>) => {
if (scrollY !== event.currentTarget.scrollTop && !ignoreScrollEvent) {
2020-08-25 20:58:36 +03:00
setScrollY(event.currentTarget.scrollTop);
2022-05-29 10:56:25 +02:00
setIgnoreScrollEvent(true);
} else {
setIgnoreScrollEvent(false);
2020-08-29 14:01:04 +03:00
}
2020-08-25 20:58:36 +03:00
};
const handleScrollX = (event: SyntheticEvent<HTMLDivElement>) => {
if (scrollX !== event.currentTarget.scrollLeft && !ignoreScrollEvent) {
2020-08-25 20:58:36 +03:00
setScrollX(event.currentTarget.scrollLeft);
2022-05-29 10:56:25 +02:00
setIgnoreScrollEvent(true);
} else {
setIgnoreScrollEvent(false);
}
};
2020-08-29 14:01:04 +03:00
/**
* Handles arrow keys events and transform it to new scroll
*/
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
2020-08-25 20:58:36 +03:00
event.preventDefault();
let newScrollY = scrollY;
let newScrollX = scrollX;
let isX = true;
switch (event.key) {
case "Down": // IE/Edge specific value
case "ArrowDown":
2020-08-25 20:58:36 +03:00
newScrollY += rowHeight;
isX = false;
break;
case "Up": // IE/Edge specific value
case "ArrowUp":
2020-08-25 20:58:36 +03:00
newScrollY -= rowHeight;
isX = false;
break;
2020-08-25 20:58:36 +03:00
case "Left":
case "ArrowLeft":
2020-08-25 20:58:36 +03:00
newScrollX -= columnWidth;
break;
case "Right": // IE/Edge specific value
case "ArrowRight":
2020-08-25 20:58:36 +03:00
newScrollX += columnWidth;
break;
}
if (isX) {
2020-08-25 20:58:36 +03:00
if (newScrollX < 0) {
2021-03-25 23:06:20 +02:00
newScrollX = 0;
} else if (newScrollX > svgWidth) {
newScrollX = svgWidth;
2020-08-25 20:58:36 +03:00
}
2021-03-25 23:06:20 +02:00
setScrollX(newScrollX);
} else {
if (newScrollY < 0) {
2021-03-25 23:06:20 +02:00
newScrollY = 0;
} else if (newScrollY > ganttFullHeight - ganttHeight) {
2021-03-25 23:06:20 +02:00
newScrollY = ganttFullHeight - ganttHeight;
}
2021-03-25 23:06:20 +02:00
setScrollY(newScrollY);
}
2020-08-29 14:01:04 +03:00
setIgnoreScrollEvent(true);
};
2020-09-15 23:23:09 +03:00
/**
* Task select event
*/
const handleSelectedTask = (taskId: string) => {
2021-03-01 21:55:27 +02:00
const newSelectedTask = barTasks.find(t => t.id === taskId);
const oldSelectedTask = barTasks.find(
t => !!selectedTask && t.id === selectedTask.id
);
if (onSelect) {
if (oldSelectedTask) {
onSelect(oldSelectedTask, false);
2020-09-15 23:23:09 +03:00
}
2021-03-01 21:55:27 +02:00
if (newSelectedTask) {
onSelect(newSelectedTask, true);
2020-09-15 23:23:09 +03:00
}
}
2021-03-01 21:55:27 +02:00
setSelectedTask(newSelectedTask);
2020-09-15 23:23:09 +03:00
};
2021-08-09 22:21:18 +03:00
const handleExpanderClick = (task: Task) => {
if (onExpanderClick && task.hideChildren !== undefined) {
onExpanderClick({ ...task, hideChildren: !task.hideChildren });
}
};
2020-07-22 20:50:43 +03:00
const gridProps: GridProps = {
columnWidth,
2021-03-25 23:06:20 +02:00
svgWidth,
2021-03-01 21:55:27 +02:00
tasks: tasks,
2020-07-22 20:50:43 +03:00
rowHeight,
2021-03-21 16:58:20 +02:00
dates: dateSetup.dates,
2020-07-30 00:01:51 +03:00
todayColor,
2021-08-03 21:27:44 +03:00
rtl,
2020-07-22 20:50:43 +03:00
};
const calendarProps: CalendarProps = {
2021-03-21 16:58:20 +02:00
dateSetup,
2020-07-22 20:50:43 +03:00
locale,
viewMode,
headerHeight,
columnWidth,
fontFamily,
fontSize,
2021-08-03 21:27:44 +03:00
rtl,
2020-07-22 20:50:43 +03:00
};
const barProps: TaskGanttContentProps = {
2021-03-01 21:55:27 +02:00
tasks: barTasks,
2021-03-21 16:58:20 +02:00
dates: dateSetup.dates,
2021-03-01 21:55:27 +02:00
ganttEvent,
2020-09-15 23:23:09 +03:00
selectedTask,
2020-07-22 20:50:43 +03:00
rowHeight,
2021-03-01 21:55:27 +02:00
taskHeight,
2020-07-22 20:50:43 +03:00
columnWidth,
arrowColor,
2020-08-11 01:16:53 +03:00
timeStep,
2020-07-22 20:50:43 +03:00
fontFamily,
fontSize,
arrowIndent,
2021-03-25 23:06:20 +02:00
svgWidth,
2021-06-28 23:15:53 +03:00
rtl,
2021-03-01 21:55:27 +02:00
setGanttEvent,
setFailedTask,
setSelectedTask: handleSelectedTask,
2020-07-22 20:50:43 +03:00
onDateChange,
onProgressChange,
onDoubleClick,
2022-02-22 16:35:56 +01:00
onClick,
2021-03-27 21:05:38 +02:00
onDelete,
2020-07-22 20:50:43 +03:00
};
const tableProps: TaskListProps = {
rowHeight,
rowWidth: listCellWidth,
fontFamily,
fontSize,
2021-03-01 21:55:27 +02:00
tasks: barTasks,
locale,
headerHeight,
scrollY,
ganttHeight,
horizontalContainerClass: styles.horizontalContainer,
2021-03-01 21:55:27 +02:00
selectedTask,
taskListRef,
2020-09-16 23:17:49 +03:00
setSelectedTask: handleSelectedTask,
2021-08-09 22:21:18 +03:00
onExpanderClick: handleExpanderClick,
TaskListHeader,
TaskListTable,
};
2020-07-22 20:50:43 +03:00
return (
<div>
<div
className={styles.wrapper}
onKeyDown={handleKeyDown}
tabIndex={0}
ref={wrapperRef}
>
{listCellWidth && <TaskList {...tableProps} />}
<TaskGantt
gridProps={gridProps}
calendarProps={calendarProps}
barProps={barProps}
ganttHeight={ganttHeight}
scrollY={scrollY}
scrollX={scrollX}
/>
{ganttEvent.changedTask && (
<Tooltip
arrowIndent={arrowIndent}
rowHeight={rowHeight}
svgContainerHeight={svgContainerHeight}
svgContainerWidth={svgContainerWidth}
fontFamily={fontFamily}
fontSize={fontSize}
scrollX={scrollX}
scrollY={scrollY}
task={ganttEvent.changedTask}
headerHeight={headerHeight}
taskListWidth={taskListWidth}
TooltipContent={TooltipContent}
2021-08-03 21:27:44 +03:00
rtl={rtl}
svgWidth={svgWidth}
/>
)}
<VerticalScroll
ganttFullHeight={ganttFullHeight}
ganttHeight={ganttHeight}
headerHeight={headerHeight}
scroll={scrollY}
onScroll={handleScrollY}
2021-08-03 21:27:44 +03:00
rtl={rtl}
/>
</div>
<HorizontalScroll
svgWidth={svgWidth}
taskListWidth={taskListWidth}
scroll={scrollX}
2021-06-28 23:15:53 +03:00
rtl={rtl}
2020-08-25 20:58:36 +03:00
onScroll={handleScrollX}
/>
</div>
2020-07-22 20:50:43 +03:00
);
};