import React, { useState, SyntheticEvent, useRef, useEffect, useMemo, } from "react"; import { ViewMode, GanttProps, Task } from "../../types/public-types"; import { GridProps } from "../grid/grid"; 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"; import { BarTask } from "../../types/bar-task"; import { convertToBarTasks } from "../../helpers/bar-helper"; import { GanttEvent } from "../../types/gantt-task-actions"; import { DateSetup } from "../../types/date-setup"; import styles from "./gantt.module.css"; import { HorizontalScroll } from "../other/horizontal-scroll"; import { removeHiddenTasks, sortTasks } from "../../helpers/other-helper"; export const Gantt: React.FunctionComponent = ({ tasks, headerHeight = 50, columnWidth = 60, listCellWidth = "155px", rowHeight = 50, ganttHeight = 0, viewMode = ViewMode.Day, locale = "en-GB", barFill = 60, barCornerRadius = 3, barProgressColor = "#a3a3ff", barProgressSelectedColor = "#8282f5", barBackgroundColor = "#b8c2cc", barBackgroundSelectedColor = "#aeb8c2", projectProgressColor = "#7db59a", projectProgressSelectedColor = "#59a985", projectBackgroundColor = "#fac465", projectBackgroundSelectedColor = "#f7bb53", milestoneBackgroundColor = "#f1c453", milestoneBackgroundSelectedColor = "#f29e4c", rtl = false, handleWidth = 8, timeStep = 300000, arrowColor = "grey", fontFamily = "Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue", fontSize = "14px", arrowIndent = 20, todayColor = "rgba(252, 248, 227, 0.5)", viewDate, TooltipContent = StandardTooltipContent, TaskListHeader = TaskListHeaderDefault, TaskListTable = TaskListTableDefault, onDateChange, onProgressChange, onDoubleClick, onDelete, onSelect, onExpanderClick, }) => { const wrapperRef = useRef(null); const taskListRef = useRef(null); const [dateSetup, setDateSetup] = useState(() => { const [startDate, endDate] = ganttDateRange(tasks, viewMode); return { viewMode, dates: seedDates(startDate, endDate, viewMode) }; }); const [currentViewDate, setCurrentViewDate] = useState( undefined ); const [taskListWidth, setTaskListWidth] = useState(0); const [svgContainerWidth, setSvgContainerWidth] = useState(0); const [svgContainerHeight, setSvgContainerHeight] = useState(ganttHeight); const [barTasks, setBarTasks] = useState([]); const [ganttEvent, setGanttEvent] = useState({ action: "", }); const taskHeight = useMemo( () => (rowHeight * barFill) / 100, [rowHeight, barFill] ); const [selectedTask, setSelectedTask] = useState(); const [failedTask, setFailedTask] = useState(null); const svgWidth = dateSetup.dates.length * columnWidth; const ganttFullHeight = barTasks.length * rowHeight; const [scrollY, setScrollY] = useState(0); const [scrollX, setScrollX] = useState(-1); const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false); // task change events useEffect(() => { let filteredTasks: Task[]; if (onExpanderClick) { filteredTasks = removeHiddenTasks(tasks); } else { filteredTasks = tasks; } filteredTasks = filteredTasks.sort(sortTasks); const [startDate, endDate] = ganttDateRange(filteredTasks, viewMode); let newDates = seedDates(startDate, endDate, viewMode); if (rtl) { newDates = newDates.reverse(); if (scrollX === -1) { setScrollX(newDates.length * columnWidth); } } setDateSetup({ dates: newDates, viewMode }); setBarTasks( convertToBarTasks( filteredTasks, newDates, columnWidth, rowHeight, taskHeight, barCornerRadius, handleWidth, rtl, barProgressColor, barProgressSelectedColor, barBackgroundColor, barBackgroundSelectedColor, projectProgressColor, projectProgressSelectedColor, projectBackgroundColor, projectBackgroundSelectedColor, milestoneBackgroundColor, milestoneBackgroundSelectedColor ) ); }, [ tasks, viewMode, rowHeight, barCornerRadius, columnWidth, taskHeight, handleWidth, barProgressColor, barProgressSelectedColor, barBackgroundColor, barBackgroundSelectedColor, projectProgressColor, projectProgressSelectedColor, projectBackgroundColor, projectBackgroundSelectedColor, milestoneBackgroundColor, milestoneBackgroundSelectedColor, rtl, scrollX, onExpanderClick, ]); 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, viewMode, currentViewDate, setCurrentViewDate, ]); 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); } }, [ganttHeight, tasks]); // 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(); } 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(); } } setIgnoreScrollEvent(true); }; // subscribe if scroll is necessary if (wrapperRef.current) { wrapperRef.current.addEventListener("wheel", handleWheel, { passive: false, }); } return () => { if (wrapperRef.current) { wrapperRef.current.removeEventListener("wheel", handleWheel); } }; }, [wrapperRef.current, scrollY, scrollX, ganttHeight, svgWidth, rtl]); const handleScrollY = (event: SyntheticEvent) => { if (scrollY !== event.currentTarget.scrollTop && !ignoreScrollEvent) { setScrollY(event.currentTarget.scrollTop); } setIgnoreScrollEvent(false); }; const handleScrollX = (event: SyntheticEvent) => { if (scrollX !== event.currentTarget.scrollLeft && !ignoreScrollEvent) { setScrollX(event.currentTarget.scrollLeft); } setIgnoreScrollEvent(false); }; /** * Handles arrow keys events and transform it to new scroll */ const handleKeyDown = (event: React.KeyboardEvent) => { event.preventDefault(); let newScrollY = scrollY; let newScrollX = scrollX; let isX = true; switch (event.key) { case "Down": // IE/Edge specific value case "ArrowDown": newScrollY += rowHeight; isX = false; break; case "Up": // IE/Edge specific value case "ArrowUp": newScrollY -= rowHeight; isX = false; break; case "Left": case "ArrowLeft": newScrollX -= columnWidth; break; case "Right": // IE/Edge specific value case "ArrowRight": newScrollX += columnWidth; break; } if (isX) { if (newScrollX < 0) { newScrollX = 0; } else if (newScrollX > svgWidth) { newScrollX = svgWidth; } setScrollX(newScrollX); } else { if (newScrollY < 0) { newScrollY = 0; } else if (newScrollY > ganttFullHeight - ganttHeight) { newScrollY = ganttFullHeight - ganttHeight; } setScrollY(newScrollY); } setIgnoreScrollEvent(true); }; /** * Task select event */ const handleSelectedTask = (taskId: string) => { 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); } if (newSelectedTask) { onSelect(newSelectedTask, true); } } setSelectedTask(newSelectedTask); }; const handleExpanderClick = (task: Task) => { if (onExpanderClick && task.hideChildren !== undefined) { onExpanderClick({ ...task, hideChildren: !task.hideChildren }); } }; const gridProps: GridProps = { columnWidth, svgWidth, tasks: tasks, rowHeight, dates: dateSetup.dates, todayColor, rtl, }; const calendarProps: CalendarProps = { dateSetup, locale, viewMode, headerHeight, columnWidth, fontFamily, fontSize, rtl, }; const barProps: TaskGanttContentProps = { tasks: barTasks, dates: dateSetup.dates, ganttEvent, selectedTask, rowHeight, taskHeight, columnWidth, arrowColor, timeStep, fontFamily, fontSize, arrowIndent, svgWidth, rtl, setGanttEvent, setFailedTask, setSelectedTask: handleSelectedTask, onDateChange, onProgressChange, onDoubleClick, onDelete, }; const tableProps: TaskListProps = { rowHeight, rowWidth: listCellWidth, fontFamily, fontSize, tasks: barTasks, locale, headerHeight, scrollY, ganttHeight, horizontalContainerClass: styles.horizontalContainer, selectedTask, taskListRef, setSelectedTask: handleSelectedTask, onExpanderClick: handleExpanderClick, TaskListHeader, TaskListTable, }; return (
{listCellWidth && } {ganttEvent.changedTask && ( )}
); };