Merge pull request #3 from MaTeMaTuK/dev

Marge for release
This commit is contained in:
MaTeMaTuK 2021-03-30 14:54:53 +03:00 committed by GitHub
commit 7ab4e7784e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 26087 additions and 13387 deletions

View File

@ -32,6 +32,7 @@
"react/no-unused-prop-types": 0, "react/no-unused-prop-types": 0,
"import/export": 0, "import/export": 0,
"no-unused-vars": "off", "no-unused-vars": "off",
"no-use-before-define": "off",
"@typescript-eslint/no-unused-vars": "error" "@typescript-eslint/no-unused-vars": "error"
} }
} }

View File

@ -24,6 +24,7 @@ let tasks: Task[] = [
end: new Date(2020, 1, 2), end: new Date(2020, 1, 2),
name: 'Idea', name: 'Idea',
id: 'Task 0', id: 'Task 0',
type:'task'
progress: 45, progress: 45,
isDisabled: true, isDisabled: true,
styles: { progressColor: '#ffbb54', progressSelectedColor: '#ff9e0d' }, styles: { progressColor: '#ffbb54', progressSelectedColor: '#ff9e0d' },
@ -71,7 +72,7 @@ npm start
| :----------------- | :---------------------------------------------------------- | :-------------------------------------------------------------------------------------- | | :----------------- | :---------------------------------------------------------- | :-------------------------------------------------------------------------------------- |
| onSelect | (task: Task, isSelected: boolean) => void | Specifies the function to be executed on the taskbar select or unselect event. | | onSelect | (task: Task, isSelected: boolean) => void | Specifies the function to be executed on the taskbar select or unselect event. |
| onDoubleClick | (task: Task) => void | Specifies the function to be executed on the taskbar onDoubleClick event. | | onDoubleClick | (task: Task) => void | Specifies the function to be executed on the taskbar onDoubleClick event. |
| onTaskDelete\* | (task: Task) => void/boolean/Promise<void>/Promise<boolean> | Specifies the function to be executed on the taskbar on Delete button press event. | | onDelete\* | (task: Task) => void/boolean/Promise<void>/Promise<boolean> | Specifies the function to be executed on the taskbar on Delete button press event. |
| onDateChange\* | (task: Task) => void/boolean/Promise<void>/Promise<boolean> | Specifies the function to be executed when drag taskbar event on timeline has finished. | | onDateChange\* | (task: Task) => void/boolean/Promise<void>/Promise<boolean> | Specifies the function to be executed when drag taskbar event on timeline has finished. |
| onProgressChange\* | (task: Task) => void/boolean/Promise<void>/Promise<boolean> | Specifies the function to be executed when drag taskbar progress event has finished. | | onProgressChange\* | (task: Task) => void/boolean/Promise<void>/Promise<boolean> | Specifies the function to be executed when drag taskbar progress event has finished. |
| timeStep | (task: Task) => number | A time step value for onDateChange. Specify in milliseconds. | | timeStep | (task: Task) => number | A time step value for onDateChange. Specify in milliseconds. |
@ -120,6 +121,7 @@ npm start
| :------------- | :------- | :---------------------------------------------------------------------------------------------------- | | :------------- | :------- | :---------------------------------------------------------------------------------------------------- |
| id\* | string | Task id. | | id\* | string | Task id. |
| name\* | string | Task display name. | | name\* | string | Task display name. |
| type\* | string | Task display type: **task**, **milestone**, **project** |
| start\* | Date | Task start date. | | start\* | Date | Task start date. |
| end\* | Date | Task end date. | | end\* | Date | Task end date. |
| progress\* | number | Task progress. Sets in percent from 0 to 100. | | progress\* | number | Task progress. Sets in percent from 0 to 100. |
@ -131,6 +133,7 @@ npm start
| | | - **progressSelectedColor**: String. Specifies the taskbar progress fill color globally on select. | | | | - **progressSelectedColor**: String. Specifies the taskbar progress fill color globally on select. |
| isDisabled | bool | Disables all action for current task. | | isDisabled | bool | Disables all action for current task. |
| fontSize | string | Specifies the taskbar font size locally. | | fontSize | string | Specifies the taskbar font size locally. |
| project | string | Task project name |
\*Required \*Required

24018
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,13 @@
import React from "react"; import React from "react";
import "gantt-task-react/dist/index.css";
import { Task, ViewMode, Gantt } from "gantt-task-react"; import { Task, ViewMode, Gantt } from "gantt-task-react";
import { ViewSwitcher } from "./components/view-switcher"; import { ViewSwitcher } from "./components/view-switcher";
import { getStartEndDateForProject, initTasks } from "./helper";
import "gantt-task-react/dist/index.css";
//Init //Init
const App = () => { const App = () => {
const currentDate = new Date();
const [view, setView] = React.useState<ViewMode>(ViewMode.Day); const [view, setView] = React.useState<ViewMode>(ViewMode.Day);
const [tasks, setTasks] = React.useState<Task[]>(initTasks());
const [isChecked, setIsChecked] = React.useState(true); const [isChecked, setIsChecked] = React.useState(true);
let columnWidth = 60; let columnWidth = 60;
if (view === ViewMode.Month) { if (view === ViewMode.Month) {
@ -14,93 +15,44 @@ const App = () => {
} else if (view === ViewMode.Week) { } else if (view === ViewMode.Week) {
columnWidth = 250; columnWidth = 250;
} }
let tasks: Task[] = [
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
end: new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
2,
12,
28
),
name: "Idea",
id: "Task 0",
progress: 45,
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 2),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4, 0, 0),
name: "Research",
id: "Task 1",
progress: 25,
dependencies: ["Task 0"],
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8, 0, 0),
name: "Discussion with team",
id: "Task 2",
progress: 10,
dependencies: ["Task 1"],
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 9, 0, 0),
name: "Developing",
id: "Task 3",
progress: 2,
dependencies: ["Task 2"],
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 10),
name: "Review",
id: "Task 4",
progress: 70,
dependencies: ["Task 2"],
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 16),
name: "Release & Eat Pizza",
id: "Task 6",
progress: currentDate.getMonth(),
dependencies: ["Task 4"],
styles: { progressColor: "#ffbb54", progressSelectedColor: "#ff9e0d" },
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 24),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 25),
name: "Closing",
id: "Task 9",
progress: 0,
isDisabled: true,
},
];
const sleep = (milliseconds: number) => { const onTaskChange = (task: Task) => {
return new Promise(resolve => setTimeout(resolve, milliseconds));
};
let onTaskChange = (task: Task) => {
console.log("On date change Id:" + task.id); console.log("On date change Id:" + task.id);
let newTasks = tasks.map(t => (t.id === task.id ? task : t));
if (task.project) {
const [start, end] = getStartEndDateForProject(newTasks, task.project);
const project = newTasks[newTasks.findIndex(t => t.id === task.project)];
if (
project.start.getTime() !== start.getTime() ||
project.end.getTime() !== end.getTime()
) {
const changedProject = { ...project, start, end };
newTasks = newTasks.map(t =>
t.id === task.project ? changedProject : t
);
}
}
setTasks(newTasks);
}; };
let onTaskDelete = (task: Task) => { const onTaskDelete = (task: Task) => {
const conf = window.confirm("Are you sure about " + task.name + " ?"); const conf = window.confirm("Are you sure about " + task.name + " ?");
if (conf) {
setTasks(tasks.filter(t => t.id !== task.id));
}
return conf; return conf;
}; };
let onProgressChange = async (task: Task) => { const onProgressChange = async (task: Task) => {
await sleep(5000); setTasks(tasks.map(t => (t.id === task.id ? task : t)));
console.log("On progress change Id:" + task.id); console.log("On progress change Id:" + task.id);
}; };
let onDblClick = (task: Task) => { const onDblClick = (task: Task) => {
alert("On Double Click event Id:" + task.id); alert("On Double Click event Id:" + task.id);
}; };
let onSelect = (task: Task, isSelected: boolean) => { const onSelect = (task: Task, isSelected: boolean) => {
console.log(task.name + " has " + (isSelected ? "selected" : "unselected")); console.log(task.name + " has " + (isSelected ? "selected" : "unselected"));
}; };
@ -116,19 +68,22 @@ const App = () => {
tasks={tasks} tasks={tasks}
viewMode={view} viewMode={view}
onDateChange={onTaskChange} onDateChange={onTaskChange}
onTaskDelete={onTaskDelete} onDelete={onTaskDelete}
onProgressChange={onProgressChange} onProgressChange={onProgressChange}
onDoubleClick={onDblClick} onDoubleClick={onDblClick}
onSelect={onSelect} onSelect={onSelect}
listCellWidth={isChecked ? "155px" : ""} listCellWidth={isChecked ? "155px" : ""}
columnWidth={columnWidth} columnWidth={columnWidth}
/> />
<h3 style={{ color: "#e56b6f" }}>
Milestones and projects are not available
</h3>
<h3>Gantt With Limited Height</h3> <h3>Gantt With Limited Height</h3>
<Gantt <Gantt
tasks={tasks} tasks={tasks}
viewMode={view} viewMode={view}
onDateChange={onTaskChange} onDateChange={onTaskChange}
onTaskDelete={onTaskDelete} onDelete={onTaskDelete}
onProgressChange={onProgressChange} onProgressChange={onProgressChange}
onDoubleClick={onDblClick} onDoubleClick={onDblClick}
onSelect={onSelect} onSelect={onSelect}

107
example/src/helper.tsx Normal file
View File

@ -0,0 +1,107 @@
import { Task } from "../../dist/types/public-types";
export function initTasks() {
const currentDate = new Date();
const tasks: Task[] = [
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
name: "Some Project",
id: "ProjectSample",
progress: 25,
type: "project",
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
end: new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
2,
12,
28
),
name: "Idea",
id: "Task 0",
progress: 45,
type: "task",
project: "ProjectSample",
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 2),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4, 0, 0),
name: "Research",
id: "Task 1",
progress: 25,
dependencies: ["Task 0"],
type: "task",
project: "ProjectSample",
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8, 0, 0),
name: "Discussion with team",
id: "Task 2",
progress: 10,
dependencies: ["Task 1"],
type: "task",
project: "ProjectSample",
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 9, 0, 0),
name: "Developing",
id: "Task 3",
progress: 2,
dependencies: ["Task 2"],
type: "task",
project: "ProjectSample",
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 10),
name: "Review",
id: "Task 4",
type: "task",
progress: 70,
dependencies: ["Task 2"],
project: "ProjectSample",
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
name: "Release",
id: "Task 6",
progress: currentDate.getMonth(),
type: "milestone",
dependencies: ["Task 4"],
project: "ProjectSample",
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 18),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 19),
name: "Party Time",
id: "Task 9",
progress: 0,
isDisabled: true,
type: "task",
},
];
return tasks;
}
export function getStartEndDateForProject(tasks: Task[], projectId: string) {
const projectTasks = tasks.filter(t => t.project === projectId);
let start = projectTasks[0].start;
let end = projectTasks[0].end;
for (let i = 0; i < projectTasks.length; i++) {
const task = projectTasks[i];
if (start.getTime() > task.start.getTime()) {
start = task.start;
}
if (end.getTime() < task.end.getTime()) {
end = task.end;
}
}
return [start, end];
}

View File

@ -26,7 +26,8 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true "noEmit": true,
"noFallthroughCasesInSwitch": true
}, },
"include": [ "include": [
"src" "src"

13855
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "gantt-task-react", "name": "gantt-task-react",
"version": "0.2.2", "version": "0.3.0",
"description": "Interactive Gantt Chart for React with TypeScript.", "description": "Interactive Gantt Chart for React with TypeScript.",
"author": "MaTeMaTuK <maksym.vikarii@gmail.com>", "author": "MaTeMaTuK <maksym.vikarii@gmail.com>",
"homepage": "https://github.com/MaTeMaTuK/gantt-task-react", "homepage": "https://github.com/MaTeMaTuK/gantt-task-react",
@ -43,30 +43,30 @@
"@testing-library/react": "^9.5.0", "@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
"@types/jest": "^25.1.4", "@types/jest": "^25.1.4",
"@types/node": "^12.12.38", "@types/node": "^12.20.4",
"@types/react": "^16.9.27", "@types/react": "^16.14.4",
"@types/react-dom": "^16.9.7", "@types/react-dom": "^16.9.11",
"@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^2.26.0", "@typescript-eslint/parser": "^4.7.0",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"cross-env": "^7.0.2", "cross-env": "^7.0.3",
"eslint-config-prettier": "^6.7.0", "eslint-config-prettier": "^6.15.0",
"eslint-config-standard": "^14.1.0", "eslint-config-standard": "^14.1.0",
"eslint-config-standard-react": "^9.2.0", "eslint-config-standard-react": "^9.2.0",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.0.0", "eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.3.1",
"eslint-plugin-react": "^7.17.0", "eslint-plugin-react": "^7.22.0",
"eslint-plugin-standard": "^4.0.1", "eslint-plugin-standard": "^4.1.0",
"gh-pages": "^2.2.0", "gh-pages": "^2.2.0",
"microbundle-crl": "^0.13.11", "microbundle-crl": "^0.13.11",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.0.4", "prettier": "^2.2.1",
"react": "^16.13.1", "react": "^16.14.0",
"react-dom": "^16.13.1", "react-dom": "^16.14.0",
"react-scripts": "^3.4.3", "react-scripts": "^4.0.3",
"typescript": "^3.8.3" "typescript": "^3.9.9"
}, },
"files": [ "files": [
"dist" "dist"

View File

@ -5,10 +5,11 @@ import {
getLocaleMonth, getLocaleMonth,
getWeekNumberISO8601, getWeekNumberISO8601,
} from "../../helpers/date-helper"; } from "../../helpers/date-helper";
import { DateSetup } from "../../types/date-setup";
import styles from "./calendar.module.css"; import styles from "./calendar.module.css";
export type CalendarProps = { export type CalendarProps = {
dates: Date[]; dateSetup: DateSetup;
locale: string; locale: string;
viewMode: ViewMode; viewMode: ViewMode;
headerHeight: number; headerHeight: number;
@ -18,7 +19,7 @@ export type CalendarProps = {
}; };
export const Calendar: React.FC<CalendarProps> = ({ export const Calendar: React.FC<CalendarProps> = ({
dates, dateSetup,
locale, locale,
viewMode, viewMode,
headerHeight, headerHeight,
@ -31,8 +32,8 @@ export const Calendar: React.FC<CalendarProps> = ({
const bottomValues: ReactChild[] = []; const bottomValues: ReactChild[] = [];
const topDefaultWidth = columnWidth * 6; const topDefaultWidth = columnWidth * 6;
const topDefaultHeight = headerHeight * 0.5; const topDefaultHeight = headerHeight * 0.5;
for (let i = 0; i < dates.length; i++) { for (let i = 0; i < dateSetup.dates.length; i++) {
const date = dates[i]; const date = dateSetup.dates[i];
const bottomValue = getLocaleMonth(date, locale); const bottomValue = getLocaleMonth(date, locale);
bottomValues.push( bottomValues.push(
<text <text
@ -44,7 +45,10 @@ export const Calendar: React.FC<CalendarProps> = ({
{bottomValue} {bottomValue}
</text> </text>
); );
if (i === 0 || date.getFullYear() !== dates[i - 1].getFullYear()) { if (
i === 0 ||
date.getFullYear() !== dateSetup.dates[i - 1].getFullYear()
) {
const topValue = date.getFullYear().toString(); const topValue = date.getFullYear().toString();
topValues.push( topValues.push(
<TopPartOfCalendar <TopPartOfCalendar
@ -69,6 +73,7 @@ export const Calendar: React.FC<CalendarProps> = ({
const bottomValues: ReactChild[] = []; const bottomValues: ReactChild[] = [];
let weeksCount: number = 1; let weeksCount: number = 1;
const topDefaultHeight = headerHeight * 0.5; const topDefaultHeight = headerHeight * 0.5;
const dates = dateSetup.dates;
for (let i = dates.length - 1; i >= 0; i--) { for (let i = dates.length - 1; i >= 0; i--) {
const date = dates[i]; const date = dates[i];
let topValue = ""; let topValue = "";
@ -116,6 +121,7 @@ export const Calendar: React.FC<CalendarProps> = ({
const topValues: ReactChild[] = []; const topValues: ReactChild[] = [];
const bottomValues: ReactChild[] = []; const bottomValues: ReactChild[] = [];
const topDefaultHeight = headerHeight * 0.5; const topDefaultHeight = headerHeight * 0.5;
const dates = dateSetup.dates;
for (let i = 0; i < dates.length; i++) { for (let i = 0; i < dates.length; i++) {
const date = dates[i]; const date = dates[i];
const bottomValue = date.getDate().toString(); const bottomValue = date.getDate().toString();
@ -157,7 +163,7 @@ export const Calendar: React.FC<CalendarProps> = ({
const bottomValues: ReactChild[] = []; const bottomValues: ReactChild[] = [];
const ticks = viewMode === ViewMode.HalfDay ? 2 : 4; const ticks = viewMode === ViewMode.HalfDay ? 2 : 4;
const topDefaultHeight = headerHeight * 0.5; const topDefaultHeight = headerHeight * 0.5;
const dates = dateSetup.dates;
for (let i = 0; i < dates.length; i++) { for (let i = 0; i < dates.length; i++) {
const date = dates[i]; const date = dates[i];
const bottomValue = Intl.DateTimeFormat(locale, { const bottomValue = Intl.DateTimeFormat(locale, {
@ -194,7 +200,7 @@ export const Calendar: React.FC<CalendarProps> = ({
}; };
let topValues: ReactChild[] = []; let topValues: ReactChild[] = [];
let bottomValues: ReactChild[] = []; let bottomValues: ReactChild[] = [];
switch (viewMode) { switch (dateSetup.viewMode) {
case ViewMode.Month: case ViewMode.Month:
[topValues, bottomValues] = getCalendarValuesForMonth(); [topValues, bottomValues] = getCalendarValuesForMonth();
break; break;
@ -213,7 +219,7 @@ export const Calendar: React.FC<CalendarProps> = ({
<rect <rect
x={0} x={0}
y={0} y={0}
width={columnWidth * dates.length} width={columnWidth * dateSetup.dates.length}
height={headerHeight} height={headerHeight}
className={styles.calendarHeader} className={styles.calendarHeader}
/> />

View File

@ -1,5 +1,5 @@
import React, { useState, SyntheticEvent, useRef, useEffect } from "react"; import React, { useState, SyntheticEvent, useRef, useEffect } from "react";
import { ViewMode, GanttProps, Task } from "../../types/public-types"; import { ViewMode, GanttProps } from "../../types/public-types";
import { GridProps } from "../grid/grid"; import { GridProps } from "../grid/grid";
import { ganttDateRange, seedDates } from "../../helpers/date-helper"; import { ganttDateRange, seedDates } from "../../helpers/date-helper";
import { CalendarProps } from "../calendar/calendar"; import { CalendarProps } from "../calendar/calendar";
@ -9,10 +9,14 @@ import { TaskListTableDefault } from "../task-list/task-list-table";
import { StandardTooltipContent } from "../other/tooltip"; import { StandardTooltipContent } from "../other/tooltip";
import { Scroll } from "../other/scroll"; import { Scroll } from "../other/scroll";
import { TaskListProps, TaskList } from "../task-list/task-list"; import { TaskListProps, TaskList } from "../task-list/task-list";
import styles from "./gantt.module.css";
import { TaskGantt } from "./task-gantt"; 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";
export const Gantt: React.SFC<GanttProps> = ({ export const Gantt: React.FunctionComponent<GanttProps> = ({
tasks, tasks,
headerHeight = 50, headerHeight = 50,
columnWidth = 60, columnWidth = 60,
@ -27,6 +31,12 @@ export const Gantt: React.SFC<GanttProps> = ({
barProgressSelectedColor = "#8282f5", barProgressSelectedColor = "#8282f5",
barBackgroundColor = "#b8c2cc", barBackgroundColor = "#b8c2cc",
barBackgroundSelectedColor = "#aeb8c2", barBackgroundSelectedColor = "#aeb8c2",
projectProgressColor = "#7db59a",
projectProgressSelectedColor = "#59a985",
projectBackgroundColor = "#fac465",
projectBackgroundSelectedColor = "#f7bb53",
milestoneBackgroundColor = "#f1c453",
milestoneBackgroundSelectedColor = "#f29e4c",
handleWidth = 8, handleWidth = 8,
timeStep = 300000, timeStep = 300000,
arrowColor = "grey", arrowColor = "grey",
@ -40,25 +50,120 @@ export const Gantt: React.SFC<GanttProps> = ({
onDateChange, onDateChange,
onProgressChange, onProgressChange,
onDoubleClick, onDoubleClick,
onTaskDelete, onDelete,
onSelect, onSelect,
}) => { }) => {
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const [ganttTasks, setGanttTasks] = useState<Task[]>(tasks); const [dateSetup, setDateSetup] = useState<DateSetup>(() => {
const [selectedTask, setSelectedTask] = useState<string>(""); const [startDate, endDate] = ganttDateRange(tasks, viewMode);
return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
});
const [taskHeight, setTaskHeight] = useState((rowHeight * barFill) / 100);
const [barTasks, setBarTasks] = useState<BarTask[]>([]);
const [ganttEvent, setGanttEvent] = useState<GanttEvent>({
action: "",
});
const [selectedTask, setSelectedTask] = useState<BarTask>();
const [failedTask, setFailedTask] = useState<BarTask | null>(null);
const [scrollY, setScrollY] = useState(0); const [scrollY, setScrollY] = useState(0);
const [scrollX, setScrollX] = useState(0); const [scrollX, setScrollX] = useState(0);
const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false); const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false);
const [startDate, endDate] = ganttDateRange(ganttTasks, viewMode);
const dates = seedDates(startDate, endDate, viewMode);
const svgHeight = rowHeight * ganttTasks.length; const svgHeight = rowHeight * barTasks.length;
const gridWidth = dates.length * columnWidth; const svgWidth = dateSetup.dates.length * columnWidth;
const ganttFullHeight = ganttTasks.length * rowHeight; const ganttFullHeight = barTasks.length * rowHeight;
// task change events
useEffect(() => {
const [startDate, endDate] = ganttDateRange(tasks, viewMode);
const newDates = seedDates(startDate, endDate, viewMode);
setDateSetup({ dates: newDates, viewMode });
setBarTasks(
convertToBarTasks(
tasks,
newDates,
columnWidth,
rowHeight,
taskHeight,
barCornerRadius,
handleWidth,
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,
]);
useEffect(() => { useEffect(() => {
setGanttTasks(tasks); const { changedTask, action } = ganttEvent;
}, [tasks]); 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(() => {
const newTaskHeight = (rowHeight * barFill) / 100;
if (newTaskHeight !== taskHeight) {
setTaskHeight(newTaskHeight);
}
}, [rowHeight, barFill, taskHeight]);
// scroll events // scroll events
useEffect(() => { useEffect(() => {
@ -79,7 +184,7 @@ export const Gantt: React.SFC<GanttProps> = ({
if ( if (
wrapperRef.current && wrapperRef.current &&
ganttHeight && ganttHeight &&
ganttHeight < ganttTasks.length * rowHeight ganttHeight < barTasks.length * rowHeight
) { ) {
wrapperRef.current.addEventListener("wheel", handleWheel, { wrapperRef.current.addEventListener("wheel", handleWheel, {
passive: false, passive: false,
@ -90,7 +195,7 @@ export const Gantt: React.SFC<GanttProps> = ({
wrapperRef.current.removeEventListener("wheel", handleWheel); wrapperRef.current.removeEventListener("wheel", handleWheel);
} }
}; };
}, [wrapperRef.current, scrollY, ganttHeight, ganttTasks, rowHeight]); }, [wrapperRef.current, scrollY, ganttHeight, barTasks, rowHeight]);
const handleScrollY = (event: SyntheticEvent<HTMLDivElement>) => { const handleScrollY = (event: SyntheticEvent<HTMLDivElement>) => {
if (scrollY !== event.currentTarget.scrollTop && !ignoreScrollEvent) { if (scrollY !== event.currentTarget.scrollTop && !ignoreScrollEvent) {
@ -136,64 +241,51 @@ export const Gantt: React.SFC<GanttProps> = ({
} }
if (isX) { if (isX) {
if (newScrollX < 0) { if (newScrollX < 0) {
setScrollX(0); newScrollX = 0;
} else if (newScrollX > gridWidth) { } else if (newScrollX > svgWidth) {
setScrollX(gridWidth); newScrollX = svgWidth;
} else {
setScrollX(newScrollX);
} }
setScrollX(newScrollX);
} else { } else {
if (newScrollY < 0) { if (newScrollY < 0) {
setScrollY(0); newScrollY = 0;
} else if (newScrollY > ganttFullHeight - ganttHeight) { } else if (newScrollY > ganttFullHeight - ganttHeight) {
setScrollY(ganttFullHeight - ganttHeight); newScrollY = ganttFullHeight - ganttHeight;
} else {
setScrollY(newScrollY);
} }
setScrollY(newScrollY);
} }
setIgnoreScrollEvent(true); setIgnoreScrollEvent(true);
}; };
// task change event
const handleTasksChange = (tasks: Task[]) => {
setGanttTasks(tasks);
};
/** /**
* Task select event * Task select event
*/ */
const handleSelectedTask = (taskId: string) => { const handleSelectedTask = (taskId: string) => {
const newSelectedTask = ganttTasks.find(t => t.id === taskId); const newSelectedTask = barTasks.find(t => t.id === taskId);
if (newSelectedTask) { const oldSelectedTask = barTasks.find(
if (onSelect) { t => !!selectedTask && t.id === selectedTask.id
const oldSelectedTask = ganttTasks.find(t => t.id === selectedTask); );
if (oldSelectedTask) { if (onSelect) {
onSelect(oldSelectedTask, false); if (oldSelectedTask) {
} onSelect(oldSelectedTask, false);
}
if (newSelectedTask) {
onSelect(newSelectedTask, true); onSelect(newSelectedTask, true);
} }
setSelectedTask(newSelectedTask.id);
} else {
if (onSelect) {
const oldSelectedTask = ganttTasks.find(t => t.id === selectedTask);
if (oldSelectedTask) {
onSelect(oldSelectedTask, false);
}
}
setSelectedTask("");
} }
setSelectedTask(newSelectedTask);
}; };
const gridProps: GridProps = { const gridProps: GridProps = {
columnWidth, columnWidth,
gridWidth, svgWidth,
tasks: ganttTasks, tasks: tasks,
rowHeight, rowHeight,
dates, dates: dateSetup.dates,
todayColor, todayColor,
}; };
const calendarProps: CalendarProps = { const calendarProps: CalendarProps = {
dates, dateSetup,
locale, locale,
viewMode, viewMode,
headerHeight, headerHeight,
@ -202,30 +294,27 @@ export const Gantt: React.SFC<GanttProps> = ({
fontSize, fontSize,
}; };
const barProps: TaskGanttContentProps = { const barProps: TaskGanttContentProps = {
tasks: ganttTasks, tasks: barTasks,
dates: dateSetup.dates,
ganttEvent,
selectedTask, selectedTask,
setSelectedTask: handleSelectedTask,
rowHeight, rowHeight,
barCornerRadius, taskHeight,
columnWidth, columnWidth,
dates,
barFill,
barProgressColor,
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor,
handleWidth,
arrowColor, arrowColor,
timeStep, timeStep,
fontFamily, fontFamily,
fontSize, fontSize,
arrowIndent, arrowIndent,
svgHeight, svgHeight,
onTasksChange: handleTasksChange, svgWidth,
setGanttEvent,
setFailedTask,
setSelectedTask: handleSelectedTask,
onDateChange, onDateChange,
onProgressChange, onProgressChange,
onDoubleClick, onDoubleClick,
onTaskDelete, onDelete,
TooltipContent, TooltipContent,
}; };
@ -234,18 +323,17 @@ export const Gantt: React.SFC<GanttProps> = ({
rowWidth: listCellWidth, rowWidth: listCellWidth,
fontFamily, fontFamily,
fontSize, fontSize,
tasks: ganttTasks, tasks: barTasks,
locale, locale,
headerHeight, headerHeight,
scrollY, scrollY,
ganttHeight, ganttHeight,
horizontalContainerClass: styles.horizontalContainer, horizontalContainerClass: styles.horizontalContainer,
selectedTaskId: selectedTask, selectedTask,
setSelectedTask: handleSelectedTask, setSelectedTask: handleSelectedTask,
TaskListHeader, TaskListHeader,
TaskListTable, TaskListTable,
}; };
return ( return (
<div <div
className={styles.wrapper} className={styles.wrapper}

View File

@ -1,91 +1,73 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Task, EventOption } from "../../types/public-types"; import { Task, EventOption } from "../../types/public-types";
import { Bar } from "../bar/bar";
import { BarTask } from "../../types/bar-task"; import { BarTask } from "../../types/bar-task";
import { Arrow } from "../other/arrow"; import { Arrow } from "../other/arrow";
import { import { handleTaskBySVGMouseEvent } from "../../helpers/bar-helper";
convertToBarTasks,
handleTaskBySVGMouseEvent,
BarMoveAction,
} from "../../helpers/bar-helper";
import { Tooltip } from "../other/tooltip"; import { Tooltip } from "../other/tooltip";
import { isKeyboardEvent } from "../../helpers/other-helper"; import { isKeyboardEvent } from "../../helpers/other-helper";
import { TaskItem } from "../task-item/task-item";
import {
BarMoveAction,
GanttContentMoveAction,
GanttEvent,
} from "../../types/gantt-task-actions";
export type GanttContentMoveAction =
| "mouseenter"
| "mouseleave"
| "delete"
| "dblclick"
| "select"
| BarMoveAction;
export type BarEvent = {
changedTask?: BarTask;
originalTask?: BarTask;
action: GanttContentMoveAction;
};
export type TaskGanttContentProps = { export type TaskGanttContentProps = {
tasks: Task[]; tasks: BarTask[];
dates: Date[]; dates: Date[];
selectedTask: string; ganttEvent: GanttEvent;
selectedTask: BarTask | undefined;
rowHeight: number; rowHeight: number;
barCornerRadius: number;
columnWidth: number; columnWidth: number;
barFill: number;
barProgressColor: string;
barProgressSelectedColor: string;
barBackgroundColor: string;
barBackgroundSelectedColor: string;
handleWidth: number;
timeStep: number; timeStep: number;
svg?: React.RefObject<SVGSVGElement>; svg?: React.RefObject<SVGSVGElement>;
svgHeight: number; svgHeight: number;
svgWidth: number;
displayXStartEndpoint?: {
start: number;
end: number;
};
taskHeight: number;
arrowColor: string; arrowColor: string;
arrowIndent: number; arrowIndent: number;
fontSize: string; fontSize: string;
fontFamily: string; fontFamily: string;
setGanttEvent: (value: GanttEvent) => void;
setFailedTask: (value: BarTask | null) => void;
setSelectedTask: (taskId: string) => void; setSelectedTask: (taskId: string) => void;
TooltipContent: React.FC<{ TooltipContent: React.FC<{
task: Task; task: Task;
fontSize: string; fontSize: string;
fontFamily: string; fontFamily: string;
}>; }>;
onTasksChange: (tasks: Task[]) => void;
} & EventOption; } & EventOption;
export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
tasks, tasks,
dates, dates,
ganttEvent,
selectedTask, selectedTask,
rowHeight, rowHeight,
barCornerRadius,
columnWidth, columnWidth,
barFill,
barProgressColor,
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor,
handleWidth,
timeStep, timeStep,
svg, svg,
svgHeight, svgHeight,
displayXStartEndpoint,
taskHeight,
arrowColor, arrowColor,
arrowIndent, arrowIndent,
fontFamily, fontFamily,
fontSize, fontSize,
setGanttEvent,
setFailedTask,
setSelectedTask, setSelectedTask,
onTasksChange,
onDateChange, onDateChange,
onProgressChange, onProgressChange,
onDoubleClick, onDoubleClick,
onTaskDelete, onDelete,
TooltipContent, TooltipContent,
}) => { }) => {
const point = svg?.current?.createSVGPoint(); const point = svg?.current?.createSVGPoint();
const [barEvent, setBarEvent] = useState<BarEvent>({
action: "",
});
const [barTasks, setBarTasks] = useState<BarTask[]>([]);
const [failedTask, setFailedTask] = useState<BarTask | null>(null);
const [xStep, setXStep] = useState(0); const [xStep, setXStep] = useState(0);
const [initEventX1Delta, setInitEventX1Delta] = useState(0); const [initEventX1Delta, setInitEventX1Delta] = useState(0);
const [isMoving, setIsMoving] = useState(false); const [isMoving, setIsMoving] = useState(false);
@ -101,51 +83,9 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
setXStep(newXStep); setXStep(newXStep);
}, [columnWidth, dates, timeStep]); }, [columnWidth, dates, timeStep]);
// generate tasks
useEffect(() => {
setBarTasks(
convertToBarTasks(
tasks,
dates,
columnWidth,
rowHeight,
barFill,
barCornerRadius,
handleWidth,
barProgressColor,
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor
)
);
}, [
tasks,
rowHeight,
barCornerRadius,
columnWidth,
dates,
barFill,
handleWidth,
barProgressColor,
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor,
]);
// on failed task update
useEffect(() => {
if (failedTask) {
const newTasks = barTasks.map(t =>
t.id === failedTask.id ? failedTask : t
);
onTasksChange(newTasks);
setFailedTask(null);
}
}, [failedTask, barTasks]);
useEffect(() => { useEffect(() => {
const handleMouseMove = async (event: MouseEvent) => { const handleMouseMove = async (event: MouseEvent) => {
if (!barEvent.changedTask || !point || !svg?.current) return; if (!ganttEvent.changedTask || !point || !svg?.current) return;
event.preventDefault(); event.preventDefault();
point.x = event.clientX; point.x = event.clientX;
@ -155,54 +95,46 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
const { isChanged, changedTask } = handleTaskBySVGMouseEvent( const { isChanged, changedTask } = handleTaskBySVGMouseEvent(
cursor.x, cursor.x,
barEvent.action as BarMoveAction, ganttEvent.action as BarMoveAction,
barEvent.changedTask, ganttEvent.changedTask,
xStep, xStep,
timeStep, timeStep,
initEventX1Delta initEventX1Delta
); );
if (isChanged) { if (isChanged) {
setBarTasks( setGanttEvent({ action: ganttEvent.action, changedTask });
barTasks.map(t => (t.id === changedTask.id ? changedTask : t))
);
setBarEvent({ ...barEvent, changedTask: changedTask });
} }
}; };
const handleMouseUp = async (event: MouseEvent) => { const handleMouseUp = async (event: MouseEvent) => {
const { changedTask: selectedTask, action, originalTask } = barEvent; const { action, originalSelectedTask, changedTask } = ganttEvent;
if (!changedTask || !point || !svg?.current || !originalSelectedTask)
if (!selectedTask || !point || !svg?.current || !originalTask) return; return;
event.preventDefault(); event.preventDefault();
point.x = event.clientX; point.x = event.clientX;
const cursor = point.matrixTransform( const cursor = point.matrixTransform(
svg?.current.getScreenCTM()?.inverse() svg?.current.getScreenCTM()?.inverse()
); );
const { changedTask: newChangedTask } = handleTaskBySVGMouseEvent(
const { changedTask } = handleTaskBySVGMouseEvent(
cursor.x, cursor.x,
action as BarMoveAction, action as BarMoveAction,
selectedTask, changedTask,
xStep, xStep,
timeStep, timeStep,
initEventX1Delta initEventX1Delta
); );
const isNotLikeOriginal = const isNotLikeOriginal =
originalTask.start !== changedTask.start || originalSelectedTask.start !== newChangedTask.start ||
originalTask.end !== changedTask.end || originalSelectedTask.end !== newChangedTask.end ||
originalTask.progress !== changedTask.progress; originalSelectedTask.progress !== newChangedTask.progress;
// remove listeners // remove listeners
svg.current.removeEventListener("mousemove", handleMouseMove); svg.current.removeEventListener("mousemove", handleMouseMove);
svg.current.removeEventListener("mouseup", handleMouseUp); svg.current.removeEventListener("mouseup", handleMouseUp);
setBarEvent({ action: "" }); setGanttEvent({ action: "" });
setIsMoving(false); setIsMoving(false);
const newTasks = barTasks.map(t =>
t.id === changedTask.id ? changedTask : t
);
onTasksChange(newTasks);
// custom operation start // custom operation start
let operationSuccess = true; let operationSuccess = true;
@ -212,7 +144,7 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
isNotLikeOriginal isNotLikeOriginal
) { ) {
try { try {
const result = await onDateChange(changedTask); const result = await onDateChange(newChangedTask);
if (result !== undefined) { if (result !== undefined) {
operationSuccess = result; operationSuccess = result;
} }
@ -221,7 +153,7 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
} }
} else if (onProgressChange && isNotLikeOriginal) { } else if (onProgressChange && isNotLikeOriginal) {
try { try {
const result = await onProgressChange(changedTask); const result = await onProgressChange(newChangedTask);
if (result !== undefined) { if (result !== undefined) {
operationSuccess = result; operationSuccess = result;
} }
@ -232,16 +164,16 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
// If operation is failed - return old state // If operation is failed - return old state
if (!operationSuccess) { if (!operationSuccess) {
setFailedTask(originalTask); setFailedTask(originalSelectedTask);
} }
}; };
if ( if (
!isMoving && !isMoving &&
(barEvent.action === "move" || (ganttEvent.action === "move" ||
barEvent.action === "end" || ganttEvent.action === "end" ||
barEvent.action === "start" || ganttEvent.action === "start" ||
barEvent.action === "progress") && ganttEvent.action === "progress") &&
svg?.current svg?.current
) { ) {
svg.current.addEventListener("mousemove", handleMouseMove); svg.current.addEventListener("mousemove", handleMouseMove);
@ -249,8 +181,7 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
setIsMoving(true); setIsMoving(true);
} }
}, [ }, [
barTasks, ganttEvent,
barEvent,
xStep, xStep,
initEventX1Delta, initEventX1Delta,
onProgressChange, onProgressChange,
@ -276,13 +207,11 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
// Keyboard events // Keyboard events
else if (isKeyboardEvent(event)) { else if (isKeyboardEvent(event)) {
if (action === "delete") { if (action === "delete") {
if (onTaskDelete) { if (onDelete) {
try { try {
const result = await onTaskDelete(task); const result = await onDelete(task);
if (result !== undefined && result) { if (result !== undefined && result) {
const newTasks = barTasks.filter(t => t.id !== task.id); setGanttEvent({ action, changedTask: task });
onTasksChange(newTasks);
setSelectedTask("");
} }
} catch (error) { } catch (error) {
console.error("Error on Delete. " + error); console.error("Error on Delete. " + error);
@ -292,16 +221,16 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
} }
// Mouse Events // Mouse Events
else if (action === "mouseenter") { else if (action === "mouseenter") {
if (!barEvent.action) { if (!ganttEvent.action) {
setBarEvent({ setGanttEvent({
action, action,
changedTask: task, changedTask: task,
originalTask: task, originalSelectedTask: task,
}); });
} }
} else if (action === "mouseleave") { } else if (action === "mouseleave") {
if (barEvent.action === "mouseenter") { if (ganttEvent.action === "mouseenter") {
setBarEvent({ action: "" }); setGanttEvent({ action: "" });
} }
} else if (action === "dblclick") { } else if (action === "dblclick") {
!!onDoubleClick && onDoubleClick(task); !!onDoubleClick && onDoubleClick(task);
@ -314,16 +243,16 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
svg.current.getScreenCTM()?.inverse() svg.current.getScreenCTM()?.inverse()
); );
setInitEventX1Delta(cursor.x - task.x1); setInitEventX1Delta(cursor.x - task.x1);
setBarEvent({ setGanttEvent({
action, action,
changedTask: task, changedTask: task,
originalTask: task, originalSelectedTask: task,
}); });
} else { } else {
setBarEvent({ setGanttEvent({
action, action,
changedTask: task, changedTask: task,
originalTask: task, originalSelectedTask: task,
}); });
} }
}; };
@ -331,14 +260,15 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
return ( return (
<g className="content"> <g className="content">
<g className="arrows" fill={arrowColor} stroke={arrowColor}> <g className="arrows" fill={arrowColor} stroke={arrowColor}>
{barTasks.map(task => { {tasks.map(task => {
return task.barChildren.map(child => { return task.barChildren.map(child => {
return ( return (
<Arrow <Arrow
key={`Arrow from ${task.id} to ${barTasks[child].id}`} key={`Arrow from ${task.id} to ${tasks[child].id}`}
taskFrom={task} taskFrom={task}
taskTo={barTasks[child]} taskTo={tasks[child]}
rowHeight={rowHeight} rowHeight={rowHeight}
taskHeight={taskHeight}
arrowIndent={arrowIndent} arrowIndent={arrowIndent}
/> />
); );
@ -346,28 +276,30 @@ export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
})} })}
</g> </g>
<g className="bar" fontFamily={fontFamily} fontSize={fontSize}> <g className="bar" fontFamily={fontFamily} fontSize={fontSize}>
{barTasks.map(task => { {tasks.map(task => {
return ( return (
<Bar <TaskItem
task={task} task={task}
arrowIndent={arrowIndent} arrowIndent={arrowIndent}
taskHeight={taskHeight}
isProgressChangeable={!!onProgressChange && !task.isDisabled} isProgressChangeable={!!onProgressChange && !task.isDisabled}
isDateChangeable={!!onDateChange && !task.isDisabled} isDateChangeable={!!onDateChange && !task.isDisabled}
isDelete={!task.isDisabled} isDelete={!task.isDisabled}
onEventStart={handleBarEventStart} onEventStart={handleBarEventStart}
key={task.id} key={task.id}
isSelected={task.id === selectedTask} isSelected={!!selectedTask && task.id === selectedTask.id}
/> />
); );
})} })}
</g> </g>
<g className="toolTip"> <g className="toolTip">
{barEvent.changedTask && ( {ganttEvent.changedTask && displayXStartEndpoint && (
<Tooltip <Tooltip
x={barEvent.changedTask.x2 + arrowIndent + arrowIndent * 0.5} arrowIndent={arrowIndent}
rowHeight={rowHeight} rowHeight={rowHeight}
svgHeight={svgHeight} svgHeight={svgHeight}
task={barEvent.changedTask} displayXStartEndpoint={displayXStartEndpoint}
task={ganttEvent.changedTask}
fontFamily={fontFamily} fontFamily={fontFamily}
fontSize={fontSize} fontSize={fontSize}
TooltipContent={TooltipContent} TooltipContent={TooltipContent}

View File

@ -1,4 +1,4 @@
import React, { useRef, useEffect, SyntheticEvent } from "react"; import React, { useRef, useEffect, SyntheticEvent, useState } from "react";
import { GridProps, Grid } from "../grid/grid"; import { GridProps, Grid } from "../grid/grid";
import { CalendarProps, Calendar } from "../calendar/calendar"; import { CalendarProps, Calendar } from "../calendar/calendar";
import { TaskGanttContentProps, TaskGanttContent } from "./task-gantt-content"; import { TaskGanttContentProps, TaskGanttContent } from "./task-gantt-content";
@ -25,7 +25,11 @@ export const TaskGantt: React.FC<TaskGanttProps> = ({
const ganttSVGRef = useRef<SVGSVGElement>(null); const ganttSVGRef = useRef<SVGSVGElement>(null);
const horizontalContainerRef = useRef<HTMLDivElement>(null); const horizontalContainerRef = useRef<HTMLDivElement>(null);
const verticalContainerRef = useRef<HTMLDivElement>(null); const verticalContainerRef = useRef<HTMLDivElement>(null);
const newBarProps = { ...barProps, svg: ganttSVGRef }; const [displayXStartEndpoint, setDisplayXStartEndpoint] = useState({
start: 0,
end: 0,
});
const newBarProps = { ...barProps, svg: ganttSVGRef, displayXStartEndpoint };
useEffect(() => { useEffect(() => {
if (horizontalContainerRef.current) { if (horizontalContainerRef.current) {
@ -36,8 +40,13 @@ export const TaskGantt: React.FC<TaskGanttProps> = ({
useEffect(() => { useEffect(() => {
if (verticalContainerRef.current) { if (verticalContainerRef.current) {
verticalContainerRef.current.scrollLeft = scrollX; verticalContainerRef.current.scrollLeft = scrollX;
setDisplayXStartEndpoint({
start: scrollX,
end: verticalContainerRef.current.clientWidth + scrollX,
});
} }
}, [scrollX]); // verticalContainerRef.current?.clientWidth need for resize window tracking
}, [scrollX, verticalContainerRef.current?.clientWidth]);
return ( return (
<div <div
@ -47,7 +56,7 @@ export const TaskGantt: React.FC<TaskGanttProps> = ({
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={gridProps.gridWidth} width={gridProps.svgWidth}
height={calendarProps.headerHeight} height={calendarProps.headerHeight}
fontFamily={barProps.fontFamily} fontFamily={barProps.fontFamily}
> >
@ -58,13 +67,13 @@ export const TaskGantt: React.FC<TaskGanttProps> = ({
className={styles.horizontalContainer} className={styles.horizontalContainer}
style={ style={
ganttHeight ganttHeight
? { height: ganttHeight, width: gridProps.gridWidth } ? { height: ganttHeight, width: gridProps.svgWidth }
: { width: gridProps.gridWidth } : { width: gridProps.svgWidth }
} }
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width={gridProps.gridWidth} width={gridProps.svgWidth}
height={barProps.rowHeight * barProps.tasks.length} height={barProps.rowHeight * barProps.tasks.length}
fontFamily={barProps.fontFamily} fontFamily={barProps.fontFamily}
ref={ganttSVGRef} ref={ganttSVGRef}

View File

@ -6,7 +6,7 @@ import styles from "./grid.module.css";
export type GridBodyProps = { export type GridBodyProps = {
tasks: Task[]; tasks: Task[];
dates: Date[]; dates: Date[];
gridWidth: number; svgWidth: number;
rowHeight: number; rowHeight: number;
columnWidth: number; columnWidth: number;
todayColor: string; todayColor: string;
@ -15,7 +15,7 @@ export const GridBody: React.FC<GridBodyProps> = ({
tasks, tasks,
dates, dates,
rowHeight, rowHeight,
gridWidth, svgWidth,
columnWidth, columnWidth,
todayColor, todayColor,
}) => { }) => {
@ -26,7 +26,7 @@ export const GridBody: React.FC<GridBodyProps> = ({
key="RowLineFirst" key="RowLineFirst"
x="0" x="0"
y1={0} y1={0}
x2={gridWidth} x2={svgWidth}
y2={0} y2={0}
className={styles.gridRowLine} className={styles.gridRowLine}
/>, />,
@ -37,7 +37,7 @@ export const GridBody: React.FC<GridBodyProps> = ({
key={"Row" + task.id} key={"Row" + task.id}
x="0" x="0"
y={y} y={y}
width={gridWidth} width={svgWidth}
height={rowHeight} height={rowHeight}
className={styles.gridRow} className={styles.gridRow}
/> />
@ -47,7 +47,7 @@ export const GridBody: React.FC<GridBodyProps> = ({
key={"RowLine" + task.id} key={"RowLine" + task.id}
x="0" x="0"
y1={y + rowHeight} y1={y + rowHeight}
x2={gridWidth} x2={svgWidth}
y2={y + rowHeight} y2={y + rowHeight}
className={styles.gridRowLine} className={styles.gridRowLine}
/> />

View File

@ -5,18 +5,20 @@ type ArrowProps = {
taskFrom: BarTask; taskFrom: BarTask;
taskTo: BarTask; taskTo: BarTask;
rowHeight: number; rowHeight: number;
taskHeight: number;
arrowIndent: number; arrowIndent: number;
}; };
export const Arrow: React.FC<ArrowProps> = ({ export const Arrow: React.FC<ArrowProps> = ({
taskFrom, taskFrom,
taskTo, taskTo,
rowHeight, rowHeight,
taskHeight,
arrowIndent, arrowIndent,
}) => { }) => {
const indexCompare = taskFrom.index > taskTo.index ? -1 : 1; const indexCompare = taskFrom.index > taskTo.index ? -1 : 1;
const taskToEndPosition = taskTo.y + taskTo.height / 2; const taskToEndPosition = taskTo.y + taskHeight / 2;
const path = `M ${taskFrom.x2} ${taskFrom.y + taskFrom.height / 2} const path = `M ${taskFrom.x2} ${taskFrom.y + taskHeight / 2}
h ${arrowIndent} h ${arrowIndent}
v ${(indexCompare * rowHeight) / 2} v ${(indexCompare * rowHeight) / 2}
H ${taskTo.x1 - arrowIndent} H ${taskTo.x1 - arrowIndent}

View File

@ -4,10 +4,14 @@ import { BarTask } from "../../types/bar-task";
import styles from "./tooltip.module.css"; import styles from "./tooltip.module.css";
export type TooltipProps = { export type TooltipProps = {
x: number;
svgHeight: number;
rowHeight: number;
task: BarTask; task: BarTask;
arrowIndent: number;
svgHeight: number;
displayXStartEndpoint: {
start: number;
end: number;
};
rowHeight: number;
fontSize: string; fontSize: string;
fontFamily: string; fontFamily: string;
TooltipContent: React.FC<{ TooltipContent: React.FC<{
@ -17,31 +21,77 @@ export type TooltipProps = {
}>; }>;
}; };
export const Tooltip: React.FC<TooltipProps> = ({ export const Tooltip: React.FC<TooltipProps> = ({
x, task,
rowHeight, rowHeight,
svgHeight, svgHeight,
task, displayXStartEndpoint,
arrowIndent,
fontSize, fontSize,
fontFamily, fontFamily,
TooltipContent, TooltipContent,
}) => { }) => {
const tooltipRef = useRef<HTMLDivElement | null>(null); const tooltipRef = useRef<HTMLDivElement | null>(null);
const [toolWidth, setToolWidth] = useState(1000); const [toolWidth, setToolWidth] = useState(1000);
const [relatedY, setRelatedY] = useState((task.index - 1) * rowHeight); const [toolHeight, setToolHeight] = useState(1000);
const [relatedY, setRelatedY] = useState(task.index * rowHeight);
const [relatedX, setRelatedX] = useState(displayXStartEndpoint.end);
useEffect(() => { useEffect(() => {
if (tooltipRef.current) { if (tooltipRef.current) {
const tooltipHeight = tooltipRef.current.offsetHeight; const tooltipHeight = tooltipRef.current.offsetHeight * 1.1;
const tooltipY = task.index * rowHeight + rowHeight; let tooltipY = task.index * rowHeight;
if (tooltipHeight > tooltipY) { const newWidth = tooltipRef.current.scrollWidth * 1.1;
setRelatedY(tooltipHeight * 0.5); let newRelatedX = task.x2 + arrowIndent + arrowIndent * 0.5;
} else if (tooltipY + tooltipHeight > svgHeight) { if (newWidth + newRelatedX > displayXStartEndpoint.end) {
setRelatedY(svgHeight - tooltipHeight * 1.05); newRelatedX = task.x1 - arrowIndent - arrowIndent * 0.5 - newWidth;
}
const tooltipLowerPoint = tooltipHeight + tooltipY;
if (
newRelatedX < displayXStartEndpoint.start &&
tooltipLowerPoint > svgHeight
) {
tooltipY -= tooltipHeight;
newRelatedX = (task.x1 + task.x2 - newWidth) * 0.5;
if (newRelatedX + newWidth > displayXStartEndpoint.end) {
newRelatedX = displayXStartEndpoint.end - newWidth;
}
if (
newRelatedX + newWidth > displayXStartEndpoint.end ||
newRelatedX - newWidth < displayXStartEndpoint.start
) {
newRelatedX = displayXStartEndpoint.end - newWidth;
}
} else if (
newRelatedX < displayXStartEndpoint.start &&
tooltipLowerPoint < svgHeight
) {
tooltipY += rowHeight;
newRelatedX = (task.x1 + task.x2 - newWidth) * 0.5;
if (
newRelatedX + newWidth > displayXStartEndpoint.end ||
newRelatedX - newWidth < displayXStartEndpoint.start
) {
newRelatedX = displayXStartEndpoint.end - newWidth;
}
} else if (tooltipLowerPoint > svgHeight) {
tooltipY = svgHeight - tooltipHeight;
}
setRelatedY(tooltipY);
setToolWidth(newWidth);
setRelatedX(newRelatedX);
if (tooltipHeight !== 1000) {
setToolHeight(tooltipHeight);
} }
setToolWidth(tooltipRef.current.scrollWidth * 1.1);
} }
}, [tooltipRef, task]); }, [tooltipRef, task, arrowIndent, displayXStartEndpoint]);
return ( return (
<foreignObject x={x} y={relatedY} width={toolWidth} height={1000}> <foreignObject
x={relatedX}
y={relatedY}
width={toolWidth}
height={toolHeight}
>
<div ref={tooltipRef} className={styles.tooltipDetailsContainer}> <div ref={tooltipRef} className={styles.tooltipDetailsContainer}>
<TooltipContent <TooltipContent
task={task} task={task}
@ -71,10 +121,13 @@ export const StandardTooltipContent: React.FC<{
}-${task.start.getFullYear()} - ${task.end.getDate()}-${ }-${task.start.getFullYear()} - ${task.end.getDate()}-${
task.end.getMonth() + 1 task.end.getMonth() + 1
}-${task.end.getFullYear()}`}</b> }-${task.end.getFullYear()}`}</b>
<p className={styles.tooltipDefaultContainerParagraph}>{`Duration: ${~~( {task.end.getTime() - task.start.getTime() !== 0 && (
(task.end.getTime() - task.start.getTime()) / <p className={styles.tooltipDefaultContainerParagraph}>{`Duration: ${~~(
(1000 * 60 * 60 * 24) (task.end.getTime() - task.start.getTime()) /
)} day(s)`}</p> (1000 * 60 * 60 * 24)
)} day(s)`}</p>
)}
<p className={styles.tooltipDefaultContainerParagraph}> <p className={styles.tooltipDefaultContainerParagraph}>
{!!task.progress && `Progress: ${task.progress} %`} {!!task.progress && `Progress: ${task.progress} %`}
</p> </p>

View File

@ -1,4 +1,4 @@
import React, { useRef, useState, useEffect } from "react"; import React from "react";
import style from "./bar.module.css"; import style from "./bar.module.css";
type BarDisplayProps = { type BarDisplayProps = {
@ -9,9 +9,6 @@ type BarDisplayProps = {
isSelected: boolean; isSelected: boolean;
progressWidth: number; progressWidth: number;
barCornerRadius: number; barCornerRadius: number;
text: string;
hasChild: boolean;
arrowIndent: number;
styles: { styles: {
backgroundColor: string; backgroundColor: string;
backgroundSelectedColor: string; backgroundSelectedColor: string;
@ -28,20 +25,9 @@ export const BarDisplay: React.FC<BarDisplayProps> = ({
isSelected, isSelected,
progressWidth, progressWidth,
barCornerRadius, barCornerRadius,
text,
hasChild,
arrowIndent,
styles, styles,
onMouseDown, onMouseDown,
}) => { }) => {
const textRef = useRef<SVGTextElement>(null);
const [isTextInside, setIsTextInside] = useState(true);
useEffect(() => {
if (textRef.current)
setIsTextInside(textRef.current.getBBox().width < width);
}, [textRef, width]);
const getProcessColor = () => { const getProcessColor = () => {
return isSelected ? styles.progressSelectedColor : styles.progressColor; return isSelected ? styles.progressSelectedColor : styles.progressColor;
}; };
@ -50,12 +36,6 @@ export const BarDisplay: React.FC<BarDisplayProps> = ({
return isSelected ? styles.backgroundSelectedColor : styles.backgroundColor; return isSelected ? styles.backgroundSelectedColor : styles.backgroundColor;
}; };
const getX = () => {
return isTextInside
? x + width * 0.5
: x + width + arrowIndent * +hasChild + arrowIndent * 0.2;
};
return ( return (
<g onMouseDown={onMouseDown}> <g onMouseDown={onMouseDown}>
<rect <rect
@ -77,18 +57,6 @@ export const BarDisplay: React.FC<BarDisplayProps> = ({
rx={barCornerRadius} rx={barCornerRadius}
fill={getProcessColor()} fill={getProcessColor()}
/> />
<text
x={getX()}
y={y + height * 0.5}
className={
isTextInside
? style.barLabel
: style.barLabel && style.barLabelOutside
}
ref={textRef}
>
{text}
</text>
</g> </g>
); );
}; };

View File

@ -0,0 +1,51 @@
import React from "react";
import {
progressWithByParams,
getProgressPoint,
} from "../../../helpers/bar-helper";
import { BarDisplay } from "./bar-display";
import { BarProgressHandle } from "./bar-progress-handle";
import { TaskItemProps } from "../task-item";
import styles from "./bar.module.css";
export const BarSmall: React.FC<TaskItemProps> = ({
task,
isProgressChangeable,
isDateChangeable,
onEventStart,
isSelected,
}) => {
const progressWidth = progressWithByParams(task.x1, task.x2, task.progress);
const progressPoint = getProgressPoint(
progressWidth + task.x1,
task.y,
task.height
);
return (
<g className={styles.barWrapper} tabIndex={0}>
<BarDisplay
x={task.x1}
y={task.y}
width={task.x2 - task.x1}
height={task.height}
progressWidth={progressWidth}
barCornerRadius={task.barCornerRadius}
styles={task.styles}
isSelected={isSelected}
onMouseDown={e => {
isDateChangeable && onEventStart("move", task, e);
}}
/>
<g className="handleGroup">
{isProgressChangeable && (
<BarProgressHandle
progressPoint={progressPoint}
onMouseDown={e => {
onEventStart("progress", task, e);
}}
/>
)}
</g>
</g>
);
};

View File

@ -0,0 +1,21 @@
.barWrapper {
cursor: pointer;
outline: none;
}
.barWrapper:hover .barHandle {
visibility: visible;
opacity: 1;
}
.barHandle {
fill: #ddd;
cursor: ew-resize;
opacity: 0;
visibility: hidden;
}
.barBackground {
user-select: none;
stroke-width: 0;
}

View File

@ -1,36 +1,19 @@
import React from "react"; import React from "react";
import { BarTask } from "../../types/bar-task";
import { import {
progressWithByParams, progressWithByParams,
getProgressPoint, getProgressPoint,
} from "../../helpers/bar-helper"; } from "../../../helpers/bar-helper";
import styles from "./bar.module.css";
import { GanttContentMoveAction } from "../gantt/task-gantt-content";
import { BarDisplay } from "./bar-display"; import { BarDisplay } from "./bar-display";
import { BarDateHandle } from "./bar-date-handle"; import { BarDateHandle } from "./bar-date-handle";
import { BarProgressHandle } from "./bar-progress-handle"; import { BarProgressHandle } from "./bar-progress-handle";
import { TaskItemProps } from "../task-item";
import styles from "./bar.module.css";
export type BarProps = { export const Bar: React.FC<TaskItemProps> = ({
task: BarTask;
arrowIndent: number;
isProgressChangeable: boolean;
isDateChangeable: boolean;
isDelete: boolean;
isSelected: boolean;
onEventStart: (
action: GanttContentMoveAction,
selectedTask: BarTask,
event?: React.MouseEvent | React.KeyboardEvent
) => any;
};
export const Bar: React.FC<BarProps> = ({
task, task,
arrowIndent,
isProgressChangeable, isProgressChangeable,
isDateChangeable, isDateChangeable,
onEventStart, onEventStart,
isDelete,
isSelected, isSelected,
}) => { }) => {
const progressWidth = progressWithByParams(task.x1, task.x2, task.progress); const progressWidth = progressWithByParams(task.x1, task.x2, task.progress);
@ -39,33 +22,9 @@ export const Bar: React.FC<BarProps> = ({
task.y, task.y,
task.height task.height
); );
const handleHeight = task.height - 2;
return ( return (
<g <g className={styles.barWrapper} tabIndex={0}>
className={styles.barWrapper}
tabIndex={0}
onKeyDown={e => {
switch (e.key) {
case "Delete": {
if (isDelete) onEventStart("delete", task, e);
break;
}
}
e.stopPropagation();
}}
onMouseEnter={e => {
onEventStart("mouseenter", task, e);
}}
onMouseLeave={e => {
onEventStart("mouseleave", task, e);
}}
onDoubleClick={e => {
onEventStart("dblclick", task, e);
}}
onFocus={() => {
onEventStart("select", task);
}}
>
<BarDisplay <BarDisplay
x={task.x1} x={task.x1}
y={task.y} y={task.y}
@ -73,9 +32,6 @@ export const Bar: React.FC<BarProps> = ({
height={task.height} height={task.height}
progressWidth={progressWidth} progressWidth={progressWidth}
barCornerRadius={task.barCornerRadius} barCornerRadius={task.barCornerRadius}
text={task.name}
hasChild={task.barChildren.length > 0}
arrowIndent={arrowIndent}
styles={task.styles} styles={task.styles}
isSelected={isSelected} isSelected={isSelected}
onMouseDown={e => { onMouseDown={e => {
@ -90,7 +46,7 @@ export const Bar: React.FC<BarProps> = ({
x={task.x1 + 1} x={task.x1 + 1}
y={task.y + 1} y={task.y + 1}
width={task.handleWidth} width={task.handleWidth}
height={task.height - 2} height={handleHeight}
barCornerRadius={task.barCornerRadius} barCornerRadius={task.barCornerRadius}
onMouseDown={e => { onMouseDown={e => {
onEventStart("start", task, e); onEventStart("start", task, e);
@ -101,7 +57,7 @@ export const Bar: React.FC<BarProps> = ({
x={task.x2 - task.handleWidth - 1} x={task.x2 - task.handleWidth - 1}
y={task.y + 1} y={task.y + 1}
width={task.handleWidth} width={task.handleWidth}
height={task.height - 2} height={handleHeight}
barCornerRadius={task.barCornerRadius} barCornerRadius={task.barCornerRadius}
onMouseDown={e => { onMouseDown={e => {
onEventStart("end", task, e); onEventStart("end", task, e);

View File

@ -0,0 +1,8 @@
.milestoneWrapper {
cursor: pointer;
outline: none;
}
.milestoneBackground {
user-select: none;
}

View File

@ -0,0 +1,37 @@
import React from "react";
import { TaskItemProps } from "../task-item";
import styles from "./milestone.module.css";
export const Milestone: React.FC<TaskItemProps> = ({
task,
isDateChangeable,
onEventStart,
isSelected,
}) => {
const transform = `rotate(45 ${task.x1 + task.height * 0.356}
${task.y + task.height * 0.85})`;
const getBarColor = () => {
return isSelected
? task.styles.backgroundSelectedColor
: task.styles.backgroundColor;
};
return (
<g tabIndex={0} className={styles.milestoneWrapper}>
<rect
fill={getBarColor()}
x={task.x1}
width={task.height}
y={task.y}
height={task.height}
rx={task.barCornerRadius}
ry={task.barCornerRadius}
transform={transform}
className={styles.milestoneBackground}
onMouseDown={e => {
isDateChangeable && onEventStart("move", task, e);
}}
/>
</g>
);
};

View File

@ -0,0 +1,13 @@
.projectWrapper {
cursor: pointer;
outline: none;
}
.projectBackground {
user-select: none;
opacity: 0.6;
}
.projectTop {
user-select: none;
}

View File

@ -0,0 +1,76 @@
import React from "react";
import { progressWithByParams } from "../../../helpers/bar-helper";
import { TaskItemProps } from "../task-item";
import styles from "./project.module.css";
export const Project: React.FC<TaskItemProps> = ({ task, isSelected }) => {
const barColor = isSelected
? task.styles.backgroundSelectedColor
: task.styles.backgroundColor;
const processColor = isSelected
? task.styles.progressSelectedColor
: task.styles.progressColor;
const progressWidth = progressWithByParams(task.x1, task.x2, task.progress);
const projectWith = task.x2 - task.x1;
const projectLeftTriangle = [
task.x1,
task.y + task.height / 2 - 1,
task.x1,
task.y + task.height,
task.x1 + 15,
task.y + task.height / 2 - 1,
].join(",");
const projectRightTriangle = [
task.x2,
task.y + task.height / 2 - 1,
task.x2,
task.y + task.height,
task.x2 - 15,
task.y + task.height / 2 - 1,
].join(",");
return (
<g tabIndex={0} className={styles.projectWrapper}>
<rect
fill={barColor}
x={task.x1}
width={projectWith}
y={task.y}
height={task.height}
rx={task.barCornerRadius}
ry={task.barCornerRadius}
className={styles.projectBackground}
/>
<rect
x={task.x1}
width={progressWidth}
y={task.y}
height={task.height}
ry={task.barCornerRadius}
rx={task.barCornerRadius}
fill={processColor}
/>
<rect
fill={barColor}
x={task.x1}
width={projectWith}
y={task.y}
height={task.height / 2}
rx={task.barCornerRadius}
ry={task.barCornerRadius}
className={styles.projectTop}
/>
<polygon
className={styles.projectTop}
points={projectLeftTriangle}
fill={barColor}
/>
<polygon
className={styles.projectTop}
points={projectRightTriangle}
fill={barColor}
/>
</g>
);
};

View File

@ -0,0 +1,110 @@
import React, { useEffect, useRef, useState } from "react";
import { BarTask } from "../../types/bar-task";
import { GanttContentMoveAction } from "../../types/gantt-task-actions";
import { Bar } from "./bar/bar";
import { BarSmall } from "./bar/bar-small";
import { Milestone } from "./milestone/milestone";
import { Project } from "./project/project";
import style from "./task-list.module.css";
export type TaskItemProps = {
task: BarTask;
arrowIndent: number;
taskHeight: number;
isProgressChangeable: boolean;
isDateChangeable: boolean;
isDelete: boolean;
isSelected: boolean;
onEventStart: (
action: GanttContentMoveAction,
selectedTask: BarTask,
event?: React.MouseEvent | React.KeyboardEvent
) => any;
};
export const TaskItem: React.FC<TaskItemProps> = props => {
const {
task,
arrowIndent,
isDelete,
taskHeight,
isSelected,
onEventStart,
} = {
...props,
};
const textRef = useRef<SVGTextElement>(null);
const [taskItem, setTaskItem] = useState<JSX.Element>(<div />);
const [isTextInside, setIsTextInside] = useState(true);
useEffect(() => {
switch (task.typeInternal) {
case "milestone":
setTaskItem(<Milestone {...props} />);
break;
case "project":
setTaskItem(<Project {...props} />);
break;
case "smalltask":
setTaskItem(<BarSmall {...props} />);
break;
default:
setTaskItem(<Bar {...props} />);
break;
}
}, [task, isSelected]);
useEffect(() => {
if (textRef.current) {
setIsTextInside(textRef.current.getBBox().width < task.x2 - task.x1);
}
}, [textRef, task]);
const getX = () => {
const width = task.x2 - task.x1;
const hasChild = task.barChildren.length > 0;
return isTextInside
? task.x1 + width * 0.5
: task.x1 + width + arrowIndent * +hasChild + arrowIndent * 0.2;
};
return (
<g
onKeyDown={e => {
switch (e.key) {
case "Delete": {
if (isDelete) onEventStart("delete", task, e);
break;
}
}
e.stopPropagation();
}}
onMouseEnter={e => {
onEventStart("mouseenter", task, e);
}}
onMouseLeave={e => {
onEventStart("mouseleave", task, e);
}}
onDoubleClick={e => {
onEventStart("dblclick", task, e);
}}
onFocus={() => {
onEventStart("select", task);
}}
>
{taskItem}
<text
x={getX()}
y={task.y + taskHeight * 0.5}
className={
isTextInside
? style.barLabel
: style.barLabel && style.barLabelOutside
}
ref={textRef}
>
{task.name}
</text>
</g>
);
};

View File

@ -1,21 +1,3 @@
.barWrapper {
cursor: pointer;
outline: none;
}
.barWrapper:hover .barHandle {
visibility: visible;
opacity: 1;
}
.barHandle {
fill: #ddd;
cursor: ew-resize;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
}
.barLabel { .barLabel {
fill: #fff; fill: #fff;
text-anchor: middle; text-anchor: middle;
@ -39,8 +21,3 @@
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
} }
.barBackground {
user-select: none;
stroke-width: 0;
}

View File

@ -59,7 +59,8 @@ export const TaskListTableDefault: React.FC<{
maxWidth: rowWidth, maxWidth: rowWidth,
}} }}
> >
&nbsp;{t.end.toLocaleDateString(locale, dateTimeOptions)} &nbsp;
{t.end.toLocaleDateString(locale, dateTimeOptions)}
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,5 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { BarTask } from "../../types/bar-task";
import { Task } from "../../types/public-types"; import { Task } from "../../types/public-types";
export type TaskListProps = { export type TaskListProps = {
@ -12,8 +13,8 @@ export type TaskListProps = {
locale: string; locale: string;
tasks: Task[]; tasks: Task[];
horizontalContainerClass?: string; horizontalContainerClass?: string;
selectedTaskId: string; selectedTask: BarTask | undefined;
setSelectedTask: (taskId: string) => void; setSelectedTask: (task: string) => void;
TaskListHeader: React.FC<{ TaskListHeader: React.FC<{
headerHeight: number; headerHeight: number;
rowWidth: string; rowWidth: string;
@ -40,7 +41,7 @@ export const TaskList: React.FC<TaskListProps> = ({
rowHeight, rowHeight,
scrollY, scrollY,
tasks, tasks,
selectedTaskId, selectedTask,
setSelectedTask, setSelectedTask,
locale, locale,
ganttHeight, ganttHeight,
@ -61,6 +62,7 @@ export const TaskList: React.FC<TaskListProps> = ({
fontSize, fontSize,
rowWidth, rowWidth,
}; };
const selectedTaskId = selectedTask ? selectedTask.id : "";
const tableProps = { const tableProps = {
rowHeight, rowHeight,
rowWidth, rowWidth,
@ -68,7 +70,7 @@ export const TaskList: React.FC<TaskListProps> = ({
fontSize, fontSize,
tasks, tasks,
locale, locale,
selectedTaskId, selectedTaskId: selectedTaskId,
setSelectedTask, setSelectedTask,
}; };

View File

@ -1,26 +1,31 @@
import { Task } from "../types/public-types"; import { Task } from "../types/public-types";
import { BarTask } from "../types/bar-task"; import { BarTask, TaskTypeInternal } from "../types/bar-task";
import { BarMoveAction } from "../types/gantt-task-actions";
export const convertToBarTasks = ( export const convertToBarTasks = (
tasks: Task[], tasks: Task[],
dates: Date[], dates: Date[],
columnWidth: number, columnWidth: number,
rowHeight: number, rowHeight: number,
barFill: number, taskHeight: number,
barCornerRadius: number, barCornerRadius: number,
handleWidth: number, handleWidth: number,
barProgressColor: string, barProgressColor: string,
barProgressSelectedColor: string, barProgressSelectedColor: string,
barBackgroundColor: string, barBackgroundColor: string,
barBackgroundSelectedColor: string barBackgroundSelectedColor: string,
projectProgressColor: string,
projectProgressSelectedColor: string,
projectBackgroundColor: string,
projectBackgroundSelectedColor: string,
milestoneBackgroundColor: string,
milestoneBackgroundSelectedColor: string
) => { ) => {
const dateDelta = const dateDelta =
dates[1].getTime() - dates[1].getTime() -
dates[0].getTime() - dates[0].getTime() -
dates[1].getTimezoneOffset() * 60 * 1000 + dates[1].getTimezoneOffset() * 60 * 1000 +
dates[0].getTimezoneOffset() * 60 * 1000; dates[0].getTimezoneOffset() * 60 * 1000;
const taskHeight = (rowHeight * barFill) / 100;
let barTasks = tasks.map((t, i) => { let barTasks = tasks.map((t, i) => {
return convertToBarTask( return convertToBarTask(
t, t,
@ -35,7 +40,13 @@ export const convertToBarTasks = (
barProgressColor, barProgressColor,
barProgressSelectedColor, barProgressSelectedColor,
barBackgroundColor, barBackgroundColor,
barBackgroundSelectedColor barBackgroundSelectedColor,
projectProgressColor,
projectProgressSelectedColor,
projectBackgroundColor,
projectBackgroundSelectedColor,
milestoneBackgroundColor,
milestoneBackgroundSelectedColor
); );
}); });
@ -54,7 +65,83 @@ export const convertToBarTasks = (
return barTasks; return barTasks;
}; };
export const convertToBarTask = ( const convertToBarTask = (
task: Task,
index: number,
dates: Date[],
dateDelta: number,
columnWidth: number,
rowHeight: number,
taskHeight: number,
barCornerRadius: number,
handleWidth: number,
barProgressColor: string,
barProgressSelectedColor: string,
barBackgroundColor: string,
barBackgroundSelectedColor: string,
projectProgressColor: string,
projectProgressSelectedColor: string,
projectBackgroundColor: string,
projectBackgroundSelectedColor: string,
milestoneBackgroundColor: string,
milestoneBackgroundSelectedColor: string
): BarTask => {
let barTask: BarTask;
switch (task.type) {
case "milestone":
barTask = convertToMilestone(
task,
index,
dates,
dateDelta,
columnWidth,
rowHeight,
taskHeight,
barCornerRadius,
handleWidth,
milestoneBackgroundColor,
milestoneBackgroundSelectedColor
);
break;
case "project":
barTask = convertToBar(
task,
index,
dates,
dateDelta,
columnWidth,
rowHeight,
taskHeight,
barCornerRadius,
handleWidth,
projectProgressColor,
projectProgressSelectedColor,
projectBackgroundColor,
projectBackgroundSelectedColor
);
break;
default:
barTask = convertToBar(
task,
index,
dates,
dateDelta,
columnWidth,
rowHeight,
taskHeight,
barCornerRadius,
handleWidth,
barProgressColor,
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor
);
break;
}
return barTask;
};
const convertToBar = (
task: Task, task: Task,
index: number, index: number,
dates: Date[], dates: Date[],
@ -69,8 +156,9 @@ export const convertToBarTask = (
barBackgroundColor: string, barBackgroundColor: string,
barBackgroundSelectedColor: string barBackgroundSelectedColor: string
): BarTask => { ): BarTask => {
debugger;
const x1 = taskXCoordinate(task.start, dates, dateDelta, columnWidth); const x1 = taskXCoordinate(task.start, dates, dateDelta, columnWidth);
const x2 = taskXCoordinate(task.end, dates, dateDelta, columnWidth); let x2 = taskXCoordinate(task.end, dates, dateDelta, columnWidth);
const y = taskYCoordinate(index, rowHeight, taskHeight); const y = taskYCoordinate(index, rowHeight, taskHeight);
const styles = { const styles = {
@ -80,8 +168,14 @@ export const convertToBarTask = (
progressSelectedColor: barProgressSelectedColor, progressSelectedColor: barProgressSelectedColor,
...task.styles, ...task.styles,
}; };
let typeInternal: TaskTypeInternal = task.type;
if (typeInternal === "task" && x2 - x1 < handleWidth * 2) {
typeInternal = "smalltask";
x2 = x1 + handleWidth * 2;
}
return { return {
...task, ...task,
typeInternal,
x1, x1,
x2, x2,
y, y,
@ -94,7 +188,51 @@ export const convertToBarTask = (
}; };
}; };
export const taskXCoordinate = ( const convertToMilestone = (
task: Task,
index: number,
dates: Date[],
dateDelta: number,
columnWidth: number,
rowHeight: number,
taskHeight: number,
barCornerRadius: number,
handleWidth: number,
milestoneBackgroundColor: string,
milestoneBackgroundSelectedColor: string
) => {
const x = taskXCoordinate(task.start, dates, dateDelta, columnWidth);
const y = taskYCoordinate(index, rowHeight, taskHeight);
const x1 = x - taskHeight * 0.5;
const x2 = x + taskHeight * 0.5;
const rotatedHeight = taskHeight / 1.414;
const styles = {
backgroundColor: milestoneBackgroundColor,
backgroundSelectedColor: milestoneBackgroundSelectedColor,
progressColor: "",
progressSelectedColor: "",
...task.styles,
};
return {
...task,
end: task.start,
x1,
x2,
y,
index,
barCornerRadius,
handleWidth,
typeInternal: task.type,
progress: 0,
height: rotatedHeight,
barChildren: [],
styles,
};
};
const taskXCoordinate = (
xDate: Date, xDate: Date,
dates: Date[], dates: Date[],
dateDelta: number, dateDelta: number,
@ -119,7 +257,7 @@ export const taskXCoordinate = (
return x; return x;
}; };
export const taskYCoordinate = ( const taskYCoordinate = (
index: number, index: number,
rowHeight: number, rowHeight: number,
taskHeight: number taskHeight: number
@ -149,7 +287,7 @@ export const progressByProgressWidth = (
} }
}; };
export const progressByX = (x: number, task: BarTask) => { const progressByX = (x: number, task: BarTask) => {
if (x >= task.x2) return 100; if (x >= task.x2) return 100;
else if (x <= task.x1) return 0; else if (x <= task.x1) return 0;
else { else {
@ -175,7 +313,7 @@ export const getProgressPoint = (
return point.join(","); return point.join(",");
}; };
export const startByX = (x: number, xStep: number, task: BarTask) => { const startByX = (x: number, xStep: number, task: BarTask) => {
if (x >= task.x2 - task.handleWidth * 2) { if (x >= task.x2 - task.handleWidth * 2) {
x = task.x2 - task.handleWidth * 2; x = task.x2 - task.handleWidth * 2;
} }
@ -185,7 +323,7 @@ export const startByX = (x: number, xStep: number, task: BarTask) => {
return newX; return newX;
}; };
export const endByX = (x: number, xStep: number, task: BarTask) => { const endByX = (x: number, xStep: number, task: BarTask) => {
if (x <= task.x1 + task.handleWidth * 2) { if (x <= task.x1 + task.handleWidth * 2) {
x = task.x1 + task.handleWidth * 2; x = task.x1 + task.handleWidth * 2;
} }
@ -195,7 +333,7 @@ export const endByX = (x: number, xStep: number, task: BarTask) => {
return newX; return newX;
}; };
export const moveByX = (x: number, xStep: number, task: BarTask) => { const moveByX = (x: number, xStep: number, task: BarTask) => {
const steps = Math.round((x - task.x1) / xStep); const steps = Math.round((x - task.x1) / xStep);
const additionalXValue = steps * xStep; const additionalXValue = steps * xStep;
const newX1 = task.x1 + additionalXValue; const newX1 = task.x1 + additionalXValue;
@ -203,7 +341,7 @@ export const moveByX = (x: number, xStep: number, task: BarTask) => {
return [newX1, newX2]; return [newX1, newX2];
}; };
export const dateByX = ( const dateByX = (
x: number, x: number,
taskX: number, taskX: number,
taskDate: Date, taskDate: Date,
@ -218,8 +356,6 @@ export const dateByX = (
return newDate; return newDate;
}; };
export type BarMoveAction = "progress" | "end" | "start" | "move" | "";
/** /**
* Method handles event in real time(mousemove) and on finish(mouseup) * Method handles event in real time(mousemove) and on finish(mouseup)
*/ */
@ -230,7 +366,41 @@ export const handleTaskBySVGMouseEvent = (
xStep: number, xStep: number,
timeStep: number, timeStep: number,
initEventX1Delta: number initEventX1Delta: number
) => { ): { isChanged: boolean; changedTask: BarTask } => {
let result: { isChanged: boolean; changedTask: BarTask };
switch (selectedTask.type) {
case "milestone":
result = handleTaskBySVGMouseEventForMilestone(
svgX,
action,
selectedTask,
xStep,
timeStep,
initEventX1Delta
);
break;
default:
result = handleTaskBySVGMouseEventForBar(
svgX,
action,
selectedTask,
xStep,
timeStep,
initEventX1Delta
);
break;
}
return result;
};
const handleTaskBySVGMouseEventForBar = (
svgX: number,
action: BarMoveAction,
selectedTask: BarTask,
xStep: number,
timeStep: number,
initEventX1Delta: number
): { isChanged: boolean; changedTask: BarTask } => {
const changedTask: BarTask = { ...selectedTask }; const changedTask: BarTask = { ...selectedTask };
let isChanged = false; let isChanged = false;
switch (action) { switch (action) {
@ -298,3 +468,39 @@ export const handleTaskBySVGMouseEvent = (
} }
return { isChanged, changedTask }; return { isChanged, changedTask };
}; };
const handleTaskBySVGMouseEventForMilestone = (
svgX: number,
action: BarMoveAction,
selectedTask: BarTask,
xStep: number,
timeStep: number,
initEventX1Delta: number
): { isChanged: boolean; changedTask: BarTask } => {
const changedTask: BarTask = { ...selectedTask };
let isChanged = false;
switch (action) {
case "move": {
const [newMoveX1, newMoveX2] = moveByX(
svgX - initEventX1Delta,
xStep,
selectedTask
);
isChanged = newMoveX1 !== selectedTask.x1;
if (isChanged) {
changedTask.start = dateByX(
newMoveX1,
selectedTask.x1,
selectedTask.start,
xStep,
timeStep
);
changedTask.end = changedTask.start;
changedTask.x1 = newMoveX1;
changedTask.x2 = newMoveX2;
}
break;
}
}
return { isChanged, changedTask };
};

View File

@ -55,7 +55,7 @@ export const startOfDate = (date: Date, scale: DateHelperScales) => {
export const ganttDateRange = (tasks: Task[], viewMode: ViewMode) => { export const ganttDateRange = (tasks: Task[], viewMode: ViewMode) => {
let newStartDate: Date = tasks[0].start; let newStartDate: Date = tasks[0].start;
let newEndDate: Date = tasks[0].end; let newEndDate: Date = tasks[0].start;
for (const task of tasks) { for (const task of tasks) {
if (task.start < newStartDate) { if (task.start < newStartDate) {
newStartDate = task.start; newStartDate = task.start;
@ -77,12 +77,18 @@ export const ganttDateRange = (tasks: Task[], viewMode: ViewMode) => {
newStartDate = addToDate(getMonday(newStartDate), -7, "day"); newStartDate = addToDate(getMonday(newStartDate), -7, "day");
newEndDate = addToDate(newEndDate, 1.5, "month"); newEndDate = addToDate(newEndDate, 1.5, "month");
break; break;
default: case ViewMode.Day:
newStartDate = startOfDate(newStartDate, "day"); newStartDate = startOfDate(newStartDate, "day");
newEndDate = startOfDate(newEndDate, "day"); newEndDate = startOfDate(newEndDate, "day");
newStartDate = addToDate(newStartDate, -1, "day"); newStartDate = addToDate(newStartDate, -1, "day");
newEndDate = addToDate(newEndDate, 19, "day"); newEndDate = addToDate(newEndDate, 19, "day");
break; break;
default:
newStartDate = startOfDate(newStartDate, "day");
newEndDate = startOfDate(newEndDate, "day");
newStartDate = addToDate(newStartDate, -1, "day");
newEndDate = addToDate(newEndDate, 5, "day");
break;
} }
return [newStartDate, newEndDate]; return [newStartDate, newEndDate];
}; };

26
src/helpers/reducer.ts Normal file
View File

@ -0,0 +1,26 @@
export function foo() {
return 1;
}
// import { BarTask } from "../types/bar-task";
// export type TaskListAction =
// | { type: GanttContentMoveAction; task: BarTask }
// | { type: "update"; tasks: BarTask[] };
// export type TaskListState = {
// tasks: BarTask[];
// changedTask?: BarTask;
// originalTask?: BarTask;
// selectedTask?: BarTask;
// activeAction: GanttContentMoveAction;
// };
// export function taskListReducer(state: TaskListState, action: TaskListAction) {
// switch (action.type) {
// case "update":
// return { ...state, tasks: action.tasks };
// case "select":
// return { ...state, selectedTask: action.task };
// default:
// return state;
// }
// }

View File

@ -14,6 +14,7 @@ describe("gantt", () => {
name: "Redesign website", name: "Redesign website",
id: "Task 0", id: "Task 0",
progress: 45, progress: 45,
type: "task",
}, },
]} ]}
/>, />,

View File

@ -1,7 +1,8 @@
import { Task } from "./public-types"; import { Task, TaskType } from "./public-types";
export interface BarTask extends Task { export interface BarTask extends Task {
index: number; index: number;
typeInternal: TaskTypeInternal;
x1: number; x1: number;
x2: number; x2: number;
y: number; y: number;
@ -16,3 +17,5 @@ export interface BarTask extends Task {
progressSelectedColor: string; progressSelectedColor: string;
}; };
} }
export type TaskTypeInternal = TaskType | "smalltask";

6
src/types/date-setup.ts Normal file
View File

@ -0,0 +1,6 @@
import { ViewMode } from "./public-types";
export interface DateSetup {
dates: Date[];
viewMode: ViewMode;
}

View File

@ -0,0 +1,17 @@
import { BarTask } from "./bar-task";
export type BarMoveAction = "progress" | "end" | "start" | "move";
export type GanttContentMoveAction =
| "mouseenter"
| "mouseleave"
| "delete"
| "dblclick"
| "select"
| ""
| BarMoveAction;
export type GanttEvent = {
changedTask?: BarTask;
originalSelectedTask?: BarTask;
action: GanttContentMoveAction;
};

View File

@ -6,8 +6,10 @@ export enum ViewMode {
Week = "Week", Week = "Week",
Month = "Month", Month = "Month",
} }
export type TaskType = "task" | "milestone" | "project";
export interface Task { export interface Task {
id: string; id: string;
type: TaskType;
name: string; name: string;
start: Date; start: Date;
end: Date; end: Date;
@ -22,6 +24,7 @@ export interface Task {
progressSelectedColor?: string; progressSelectedColor?: string;
}; };
isDisabled?: boolean; isDisabled?: boolean;
project?: string;
dependencies?: string[]; dependencies?: string[];
} }
@ -53,9 +56,7 @@ export interface EventOption {
/** /**
* Invokes on delete selected task. Chart undoes operation if method return false or error. * Invokes on delete selected task. Chart undoes operation if method return false or error.
*/ */
onTaskDelete?: ( onDelete?: (task: Task) => void | boolean | Promise<void> | Promise<boolean>;
task: Task
) => void | boolean | Promise<void> | Promise<boolean>;
} }
export interface DisplayOption { export interface DisplayOption {
@ -85,6 +86,12 @@ export interface StylingOption {
barProgressSelectedColor?: string; barProgressSelectedColor?: string;
barBackgroundColor?: string; barBackgroundColor?: string;
barBackgroundSelectedColor?: string; barBackgroundSelectedColor?: string;
projectProgressColor?: string;
projectProgressSelectedColor?: string;
projectBackgroundColor?: string;
projectBackgroundSelectedColor?: string;
milestoneBackgroundColor?: string;
milestoneBackgroundSelectedColor?: string;
arrowColor?: string; arrowColor?: string;
arrowIndent?: number; arrowIndent?: number;
todayColor?: string; todayColor?: string;