gantt and timeline has separated, added OOB list

This commit is contained in:
unknown 2020-08-23 22:33:25 +03:00
parent ac061d6cb6
commit 764b027bd8
30 changed files with 713 additions and 324 deletions

View File

@ -84,26 +84,32 @@ npm start
### StylingOption
| Parameter Name | Type | Description |
| :------------------------- | :----- | :---------------------------------------------------------------------- |
| headerHeight | number | Specifies the header height. |
| columnWidth | number | Specifies the time period width. |
| rowHeight | number | Specifies the task row height. |
| barCornerRadius | number | Specifies the taskbar corner rounding. |
| barFill | number | Specifies the taskbar occupation. Sets in percent from 0 to 100. |
| handleWidth | number | Specifies width the taskbar drag event control for start and end dates. |
| fontFamily | string | Specifies the application font. |
| fontSize | string | Specifies the application font size. |
| barProgressColor | string | Specifies the taskbar progress fill color globally. |
| barProgressSelectedColor | string | Specifies the taskbar progress fill color globally on select. |
| barBackgroundColor | string | Specifies the taskbar background fill color globally. |
| barBackgroundSelectedColor | string | Specifies the taskbar background fill color globally on select. |
| arrowColor | string | Specifies the relationship arrow fill color. |
| arrowIndent | number | Specifies the relationship arrow right indent. Sets in px |
| todayColor | string | Specifies the current period column fill color. |
| getTooltipContent | \*\* | Specifies the Tooltip for selected taskbar. |
| Parameter Name | Type | Description |
| :------------------------- | :------- | :--------------------------------------------------------------------------------------------- |
| headerHeight | number | Specifies the header height. |
| ganttHeight | number | Specifies the gantt chart height without header. Default is 0. It`s mean no height limitation. |
| columnWidth | number | Specifies the time period width. |
| listCellWidth | string | Specifies the task list cell width. Empty string is mean "no display". |
| rowHeight | number | Specifies the task row height. |
| barCornerRadius | number | Specifies the taskbar corner rounding. |
| barFill | number | Specifies the taskbar occupation. Sets in percent from 0 to 100. |
| handleWidth | number | Specifies width the taskbar drag event control for start and end dates. |
| fontFamily | string | Specifies the application font. |
| fontSize | string | Specifies the application font size. |
| barProgressColor | string | Specifies the taskbar progress fill color globally. |
| barProgressSelectedColor | string | Specifies the taskbar progress fill color globally on select. |
| barBackgroundColor | string | Specifies the taskbar background fill color globally. |
| barBackgroundSelectedColor | string | Specifies the taskbar background fill color globally on select. |
| arrowColor | string | Specifies the relationship arrow fill color. |
| arrowIndent | number | Specifies the relationship arrow right indent. Sets in px |
| todayColor | string | Specifies the current period column fill color. |
| TooltipContent | \*\* | Specifies the Tooltip view for selected taskbar. |
| TaskListHeader | \*\*\* | Specifies the task list Header view |
| TaskListTable | \*\*\*\* | Specifies the task list Table view |
[\*\*`(task:Task, fontSize:string , fontFamily:string) => JSX.Element;`](https://github.com/MaTeMaTuK/gantt-task-react/blob/07dfeddd4d96ecc418619cad9cd9ba3c31bb82a8/src/components/Other/tooltip.tsx#L47)
[\*\*`React.FC<{ task: Task; fontSize: string; fontFamily: string; }>;`](https://github.com/MaTeMaTuK/gantt-task-react/blob/07dfeddd4d96ecc418619cad9cd9ba3c31bb82a8/src/components/Other/tooltip.tsx#L47)
\*\*\*React.SFC<{ headerHeight: number; rowWidth: string; fontFamily: string; fontSize: string;}>;
\*\*\*\*TaskListTable: React.SFC<{ rowHeight: number; rowWidth: string; fontFamily: string; fontSize: string; locale: string; tasks: Task[]; }>;
### Task

View File

@ -1,13 +1,19 @@
import React from "react";
import "gantt-task-react/dist/index.css";
import { Task, ViewMode } from "gantt-task-react";
import { Task, ViewMode, Gantt } from "gantt-task-react";
import { ViewSwitcher } from "./components/view-switcher";
import { GanttTableExample } from "./components/gantt-table";
//Init
const App = () => {
const currentDate = new Date();
const [view, setView] = React.useState<ViewMode>(ViewMode.Day);
const [isChecked, setIsChecked] = React.useState(true);
let columnWidth = 60;
if (view === ViewMode.Month) {
columnWidth = 300;
} else if (view === ViewMode.Week) {
columnWidth = 250;
}
let tasks: Task[] = [
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
@ -21,7 +27,6 @@ const App = () => {
name: "Idea",
id: "Task 0",
progress: 45,
isDisabled: true,
},
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 2),
@ -58,12 +63,20 @@ const App = () => {
{
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 16),
name: "Release & Eat Burgers",
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,
},
];
let onTaskChange = (task: Task) => {
@ -85,14 +98,33 @@ const App = () => {
return (
<div>
<ViewSwitcher onViewChange={viewMode => setView(viewMode)} />
<GanttTableExample
<ViewSwitcher
onViewModeChange={viewMode => setView(viewMode)}
onViewListChange={setIsChecked}
isChecked={isChecked}
/>
<h3>Gantt With Unlimited Height</h3>
<Gantt
tasks={tasks}
viewMode={view}
onDateChange={onTaskChange}
onTaskDelete={onTaskDelete}
onProgressChange={onProgressChange}
onDoubleClick={onDblClick}
listCellWidth={isChecked ? "150px" : ""}
columnWidth={columnWidth}
/>
<h3>Gantt With Limited Height</h3>
<Gantt
tasks={tasks}
viewMode={view}
onDateChange={onTaskChange}
onTaskDelete={onTaskDelete}
onProgressChange={onProgressChange}
onDoubleClick={onDblClick}
listCellWidth={isChecked ? "150px" : ""}
ganttHeight={300}
columnWidth={columnWidth}
/>
</div>
);

View File

@ -1,96 +0,0 @@
import React from "react";
import "gantt-task-react/dist/index.css";
import {
Gantt,
Task,
EventOption,
StylingOption,
ViewMode,
DisplayOption,
} from "gantt-task-react";
//Gantt with Custom table example
type GanttTableExampleProps = { tasks: Task[] } & EventOption & DisplayOption;
export const GanttTableExample: React.SFC<GanttTableExampleProps> = props => {
const gridColumnWidth = 150;
let options: StylingOption = {
fontSize: "14px",
fontFamily:
"Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue",
headerHeight: 50,
rowHeight: 50,
};
if (props.viewMode === ViewMode.Month) {
options.columnWidth = 300;
} else if (props.viewMode === ViewMode.Week) {
options.columnWidth = 250;
}
return (
<div className="Wrapper">
<div
className="GanttTable"
style={{
fontFamily: options.fontFamily,
fontSize: options.fontSize,
}}
>
<div
className="GanttTable-header"
style={{
height: options.headerHeight,
}}
>
<div
className="GanttTable-headerItem"
style={{
minWidth: gridColumnWidth,
}}
>
<span role="img" aria-label="fromDate" className="GanttTable-icon">
📃
</span>
Name
</div>
<div
className="GanttTable-headerItem"
style={{
minWidth: gridColumnWidth,
}}
>
<span role="img" aria-label="fromDate" className="GanttTable-icon">
📅
</span>
From
</div>
<div
className="GanttTable-headerItem"
style={{
minWidth: gridColumnWidth,
}}
>
<span role="img" aria-label="toDate" className="GanttTable-icon">
📅
</span>
To
</div>
</div>
{props.tasks.map(t => {
return (
<div
className="GanttTable-row"
style={{ height: options.rowHeight }}
>
<div className="GanttTable-cell">{t.name}</div>
<div className="GanttTable-cell">{t.start.toDateString()}</div>
<div className="GanttTable-cell">{t.end.toDateString()}</div>
</div>
);
})}
</div>
<div style={{ overflowX: "scroll" }}>
<Gantt {...props} {...options} />
</div>
</div>
);
};

View File

@ -2,31 +2,56 @@ import React from "react";
import "gantt-task-react/dist/index.css";
import { ViewMode } from "gantt-task-react";
type ViewSwitcherProps = {
onViewChange: (viewMode: ViewMode) => void;
isChecked: boolean;
onViewListChange: (isChecked: boolean) => void;
onViewModeChange: (viewMode: ViewMode) => void;
};
export const ViewSwitcher: React.SFC<ViewSwitcherProps> = ({
onViewChange,
onViewModeChange,
onViewListChange,
isChecked,
}) => {
return (
<div className="ViewContainer">
<button
className="Button"
onClick={() => onViewChange(ViewMode.QuarterDay)}
onClick={() => onViewModeChange(ViewMode.QuarterDay)}
>
Quarter of Day
</button>
<button className="Button" onClick={() => onViewChange(ViewMode.HalfDay)}>
<button
className="Button"
onClick={() => onViewModeChange(ViewMode.HalfDay)}
>
Half of Day
</button>
<button className="Button" onClick={() => onViewChange(ViewMode.Day)}>
<button className="Button" onClick={() => onViewModeChange(ViewMode.Day)}>
Day
</button>
<button className="Button" onClick={() => onViewChange(ViewMode.Week)}>
<button
className="Button"
onClick={() => onViewModeChange(ViewMode.Week)}
>
Week
</button>
<button className="Button" onClick={() => onViewChange(ViewMode.Month)}>
<button
className="Button"
onClick={() => onViewModeChange(ViewMode.Month)}
>
Month
</button>
<div className="Switch">
<label className="Switch_Toggle">
<input
type="checkbox"
defaultChecked={isChecked}
onClick={() => onViewListChange(!isChecked)}
/>
<span className="Slider" />
</label>
Show Task List
</div>
</div>
);
};

View File

@ -1,57 +1,10 @@
/*Styles for Example Table*/
.Wrapper {
display: flex;
justify-content: space-around;
background: #ffff;
padding: 0;
margin: 0;
list-style: none;
}
.GanttTable {
display: table;
}
.GanttTable-header {
display: table-row;
list-style: none;
}
.GanttTable-headerItem {
display: table-cell;
padding-top: 24px;
vertical-align: text-bottom;
border-left: #e6e4e4 1px solid;
border-top: #e6e4e4 1px solid;
border-bottom: #e6e4e4 1px solid;
}
.GanttTable-row {
display: table-row;
text-overflow: ellipsis;
}
.GanttTable-row:nth-of-type(odd) {
background-color: #f5f5f5;
}
.GanttTable-cell {
display: table-cell;
vertical-align: middle;
border-left: #e6e4e4 1px solid;
}
.GanttTable-icon {
padding-right: 2px;
}
.ViewContainer {
list-style: none;
-ms-box-orient: horizontal;
display: flex;
-webkit-justify-content: flex-end;
justify-content: flex-end;
align-items: center;
}
.Button {
@ -59,9 +12,68 @@
color: black;
border: none;
padding: 7px 16px;
text-align: center;
text-decoration: none;
font-size: 14px;
margin: 4px 2px;
cursor: pointer;
font-size: 14px;
text-align: center;
}
.Switch {
margin: 4px 15px;
font-size: 14px;
font-family: "Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue";
display: flex;
justify-content: center;
align-items: center;
}
.Switch_Toggle {
position: relative;
display: inline-block;
width: 60px;
height: 30px;
margin-right: 5px;
}
.Switch_Toggle input {
opacity: 0;
width: 0;
height: 0;
}
.Slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: 0.4s;
transition: 0.4s;
}
.Slider:before {
position: absolute;
content: "";
height: 21px;
width: 21px;
left: 6px;
bottom: 4px;
background-color: white;
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .Slider {
background-color: #2196f3;
}
input:focus + .Slider {
box-shadow: 0 0 1px #2196f3;
}
input:checked + .Slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}

View File

@ -1,6 +1,6 @@
{
"name": "gantt-task-react",
"version": "0.1.2",
"version": "0.1.3",
"description": "Interactive Gantt Chart for React with TypeScript.",
"author": "MaTeMaTuK <maksym.vikarii@gmail.com>",
"homepage": "https://github.com/MaTeMaTuK/gantt-task-react",

View File

@ -32,6 +32,12 @@
.barLabelOutside {
fill: #555;
text-anchor: start;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}
.barBackground {

View File

@ -1,14 +1,14 @@
import React, { useState } from "react";
import { BarProgressHandle } from "./bar-progress-handle";
import { BarDateHandle } from "./bar-date-handle";
import { BarDisplay } from "./bar-display";
import { BarTask } from "../../types/bar-task";
import {
progressWithByParams,
getProgressPoint,
} from "../../helpers/bar-helper";
import styles from "./bar.module.css";
import { GanttContentMoveAction } from "../Gantt/gantt-content";
import { GanttContentMoveAction } from "../gantt/task-gantt-content";
import { BarDisplay } from "./bar-display";
import { BarDateHandle } from "./bar-date-handle";
import { BarProgressHandle } from "./bar-progress-handle";
export type BarProps = {
task: BarTask;

View File

@ -23,3 +23,9 @@
user-select: none;
pointer-events: none;
}
.calendarHeader {
fill: #ffffff;
stroke: #e0e0e0;
stroke-width: 1.4;
}

View File

@ -67,7 +67,7 @@ export const Calendar: React.FC<CalendarProps> = ({
const getCalendarValuesForWeek = () => {
const topValues: ReactChild[] = [];
const bottomValues: ReactChild[] = [];
let weeksCount: number = 0;
let weeksCount: number = 1;
const topDefaultHeight = headerHeight * 0.5;
for (let i = dates.length - 1; i >= 0; i--) {
const date = dates[i];
@ -138,7 +138,7 @@ export const Calendar: React.FC<CalendarProps> = ({
topValues.push(
<TopPartOfCalendar
key={topValue}
key={topValue + date.getFullYear()}
value={topValue}
x1Line={columnWidth * (i + 1)}
y1Line={0}
@ -179,7 +179,7 @@ export const Calendar: React.FC<CalendarProps> = ({
const topValue = `${date.getDate()} ${getLocaleMonth(date, locale)}`;
topValues.push(
<TopPartOfCalendar
key={topValue}
key={topValue + date.getFullYear()}
value={topValue}
x1Line={columnWidth * i + ticks * columnWidth}
y1Line={0}
@ -210,6 +210,13 @@ export const Calendar: React.FC<CalendarProps> = ({
}
return (
<g className="calendar" fontSize={fontSize} fontFamily={fontFamily}>
<rect
x={0}
y={0}
width={columnWidth * dates.length}
height={headerHeight}
className={styles.calendarHeader}
/>
{bottomValues} {topValues}
</g>
);

View File

@ -0,0 +1,20 @@
.ganttVerticalContainer {
overflow-x: auto;
overflow-y: hidden;
font-size: 0;
margin: 0;
padding: 0;
}
.horizontalContainer {
margin: 0;
padding: 0;
overflow-y: hidden;
}
.wrapper {
display: flex;
padding: 0;
margin: 0;
list-style: none;
}

View File

@ -1,15 +1,24 @@
import React, { useRef, useState } from "react";
import React, { useState, SyntheticEvent } from "react";
import { ViewMode, GanttProps, Task } from "../../types/public-types";
import { Grid, GridProps } from "../Grid/grid";
import { Calendar, CalendarProps } from "../Calendar/calendar";
import { GanttContent, GanttContentProps } from "./gantt-content";
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 } from "../other/tooltip";
import { Scroll } from "../other/scroll";
import { TaskListProps, TaskList } from "../task-list/task-list";
import styles from "./gantt.module.css";
import { TaskGantt } from "./task-gantt";
export const Gantt: React.SFC<GanttProps> = ({
tasks,
headerHeight = 50,
columnWidth = 60,
listCellWidth = "150px",
rowHeight = 50,
ganttHeight = 0,
viewMode = ViewMode.Day,
locale = "en-GB",
barFill = 60,
@ -25,27 +34,36 @@ export const Gantt: React.SFC<GanttProps> = ({
fontSize = "14px",
arrowIndent = 20,
todayColor = "rgba(252, 248, 227, 0.5)",
TooltipContent = StandardTooltipContent,
TaskListHeader = TaskListHeaderDefault,
TaskListTable = TaskListTableDefault,
onDateChange,
onProgressChange,
onDoubleClick,
onTaskDelete,
getTooltipContent,
}) => {
const svg = useRef<SVGSVGElement>(null);
const [ganttTasks, setGanttTasks] = useState<Task[]>(tasks);
const [scroll, setScroll] = useState(0);
const [startDate, endDate] = ganttDateRange(ganttTasks, viewMode);
const dates = seedDates(startDate, endDate, viewMode);
const handleOnTasksChange = (tasks: Task[]) => {
const svgHeight = rowHeight * tasks.length;
const gridWidth = dates.length * columnWidth;
const onTasksDateChange = (tasks: Task[]) => {
setGanttTasks(tasks);
};
const handleScroll = (event: SyntheticEvent<HTMLDivElement>) => {
setScroll(event.currentTarget.scrollTop);
};
const gridProps: GridProps = {
columnWidth,
gridWidth: dates.length * columnWidth,
gridWidth,
tasks: ganttTasks,
rowHeight,
headerHeight,
dates,
todayColor,
};
@ -58,7 +76,7 @@ export const Gantt: React.SFC<GanttProps> = ({
fontFamily,
fontSize,
};
const barProps: GanttContentProps = {
const barProps: TaskGanttContentProps = {
tasks: ganttTasks,
rowHeight,
barCornerRadius,
@ -69,32 +87,52 @@ export const Gantt: React.SFC<GanttProps> = ({
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor,
headerHeight,
handleWidth,
arrowColor,
timeStep,
fontFamily,
fontSize,
arrowIndent,
svg,
onTasksDateChange: handleOnTasksChange,
svgHeight,
onTasksDateChange: onTasksDateChange,
onDateChange,
onProgressChange,
onDoubleClick,
onTaskDelete,
getTooltipContent,
TooltipContent,
};
const tableProps: TaskListProps = {
rowHeight,
rowWidth: listCellWidth,
fontFamily,
fontSize,
tasks: ganttTasks,
locale,
headerHeight,
scroll,
ganttHeight,
horizontalContainerClass: styles.horizontalContainer,
TaskListHeader,
TaskListTable,
};
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={columnWidth * dates.length}
height={headerHeight + rowHeight * tasks.length}
fontFamily={fontFamily}
ref={svg}
>
<Grid {...gridProps} />
<Calendar {...calendarProps} />
<GanttContent {...barProps} />
</svg>
<div className={styles.wrapper}>
{listCellWidth && <TaskList {...tableProps} />}
<TaskGantt
gridProps={gridProps}
calendarProps={calendarProps}
barProps={barProps}
ganttHeight={ganttHeight}
scroll={scroll}
/>
<Scroll
ganttFullHeight={ganttTasks.length * rowHeight}
ganttHeight={ganttHeight}
headerHeight={headerHeight}
onScroll={handleScroll}
/>
</div>
);
};

View File

@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react";
import { Task, EventOption } from "../../types/public-types";
import { Bar } from "../Bar/bar";
import { Bar } from "../bar/bar";
import { BarTask } from "../../types/bar-task";
import { Arrow } from "../Other/arrow";
import { Arrow } from "../other/arrow";
import {
convertToBarTasks,
handleTaskBySVGMouseEvent,
BarMoveAction,
} from "../../helpers/bar-helper";
import { Tooltip } from "../Other/tooltip";
import { Tooltip } from "../other/tooltip";
import { isKeyboardEvent } from "../../helpers/other-helper";
export type GanttContentMoveAction =
@ -21,7 +21,7 @@ export type BarEvent = {
selectedTask?: BarTask;
action: GanttContentMoveAction;
};
export type GanttContentProps = {
export type TaskGanttContentProps = {
tasks: Task[];
dates: Date[];
rowHeight: number;
@ -32,23 +32,23 @@ export type GanttContentProps = {
barProgressSelectedColor: string;
barBackgroundColor: string;
barBackgroundSelectedColor: string;
headerHeight: number;
handleWidth: number;
timeStep: number;
svg: React.RefObject<SVGSVGElement>;
svg?: React.RefObject<SVGSVGElement>;
svgHeight: number;
arrowColor: string;
arrowIndent: number;
fontSize: string;
fontFamily: string;
getTooltipContent?: (
task: Task,
fontSize: string,
fontFamily: string
) => JSX.Element;
TooltipContent: React.FC<{
task: Task;
fontSize: string;
fontFamily: string;
}>;
onTasksDateChange: (tasks: Task[]) => void;
} & EventOption;
export const GanttContent: React.FC<GanttContentProps> = ({
export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
tasks,
dates,
rowHeight,
@ -59,10 +59,10 @@ export const GanttContent: React.FC<GanttContentProps> = ({
barProgressSelectedColor,
barBackgroundColor,
barBackgroundSelectedColor,
headerHeight,
handleWidth,
timeStep,
svg,
svgHeight,
arrowColor,
arrowIndent,
fontFamily,
@ -72,9 +72,9 @@ export const GanttContent: React.FC<GanttContentProps> = ({
onProgressChange,
onDoubleClick,
onTaskDelete,
getTooltipContent,
TooltipContent,
}) => {
const point = svg.current?.createSVGPoint();
const point = svg?.current?.createSVGPoint();
const [barEvent, setBarEvent] = useState<BarEvent>({
action: "",
});
@ -103,7 +103,6 @@ export const GanttContent: React.FC<GanttContentProps> = ({
columnWidth,
rowHeight,
barFill,
headerHeight,
barCornerRadius,
handleWidth,
barProgressColor,
@ -120,7 +119,6 @@ export const GanttContent: React.FC<GanttContentProps> = ({
dates,
barFill,
handleWidth,
headerHeight,
barProgressColor,
barProgressSelectedColor,
barBackgroundColor,
@ -139,9 +137,7 @@ export const GanttContent: React.FC<GanttContentProps> = ({
if (action === "delete") {
if (onTaskDelete) {
await onTaskDelete(selectedTask);
const newTasks = barTasks.filter(
t => t.id !== barEvent.selectedTask?.id
);
const newTasks = barTasks.filter(t => t.id !== selectedTask.id);
onTasksDateChange(newTasks);
}
}
@ -154,7 +150,7 @@ export const GanttContent: React.FC<GanttContentProps> = ({
setBarEvent({ action: "" });
}
} else if (action === "move") {
if (!svg.current || !point) return;
if (!svg?.current || !point) return;
point.x = event.clientX;
const cursor = point.matrixTransform(
svg.current.getScreenCTM()?.inverse()
@ -173,12 +169,12 @@ export const GanttContent: React.FC<GanttContentProps> = ({
useEffect(() => {
const handleMouseMove = async (event: MouseEvent) => {
if (!barEvent.selectedTask || !point || !svg.current) return;
if (!barEvent.selectedTask || !point || !svg?.current) return;
event.preventDefault();
point.x = event.clientX;
const cursor = point.matrixTransform(
svg.current.getScreenCTM()?.inverse()
svg?.current.getScreenCTM()?.inverse()
);
const { isChanged, changedTask } = handleTaskBySVGMouseEvent(
@ -199,12 +195,12 @@ export const GanttContent: React.FC<GanttContentProps> = ({
const handleMouseUp = async (event: MouseEvent) => {
const { selectedTask, action } = barEvent;
if (!selectedTask || !point || !svg.current) return;
if (!selectedTask || !point || !svg?.current) return;
event.preventDefault();
point.x = event.clientX;
const cursor = point.matrixTransform(
svg.current.getScreenCTM()?.inverse()
svg?.current.getScreenCTM()?.inverse()
);
const { changedTask } = handleTaskBySVGMouseEvent(
@ -239,7 +235,7 @@ export const GanttContent: React.FC<GanttContentProps> = ({
barEvent.action === "end" ||
barEvent.action === "start" ||
barEvent.action === "progress") &&
svg.current
svg?.current
) {
svg.current.addEventListener("mousemove", handleMouseMove);
svg.current.addEventListener("mouseup", handleMouseUp);
@ -282,7 +278,7 @@ export const GanttContent: React.FC<GanttContentProps> = ({
arrowIndent={arrowIndent}
isProgressChangeable={!!onProgressChange && !task.isDisabled}
isDateChangeable={!!onDateChange && !task.isDisabled}
isDelete={!!onTaskDelete && !task.isDisabled}
isDelete={!task.isDisabled}
onEventStart={handleBarEventStart}
key={task.id}
/>
@ -293,11 +289,12 @@ export const GanttContent: React.FC<GanttContentProps> = ({
{barEvent.selectedTask && (
<Tooltip
x={barEvent.selectedTask.x2 + arrowIndent + arrowIndent * 0.5}
y={barEvent.selectedTask.y + rowHeight}
rowHeight={rowHeight}
svgHeight={svgHeight}
task={barEvent.selectedTask}
fontFamily={fontFamily}
fontSize={fontSize}
getTooltipContent={getTooltipContent}
TooltipContent={TooltipContent}
/>
)}
</g>

View File

@ -0,0 +1,63 @@
import React, { useRef, useEffect } from "react";
import { GridProps, Grid } from "../grid/grid";
import { CalendarProps, Calendar } from "../calendar/calendar";
import { TaskGanttContentProps, TaskGanttContent } from "./task-gantt-content";
import styles from "./gantt.module.css";
export type TaskGanttProps = {
gridProps: GridProps;
calendarProps: CalendarProps;
barProps: TaskGanttContentProps;
ganttHeight: number;
scroll: number;
};
export const TaskGantt: React.FC<TaskGanttProps> = ({
gridProps,
calendarProps,
barProps,
ganttHeight,
scroll,
}) => {
const ganttSVGRef = useRef<SVGSVGElement>(null);
const horizontalContainerRef = useRef<HTMLDivElement>(null);
const newBarProps = { ...barProps, svg: ganttSVGRef };
useEffect(() => {
if (horizontalContainerRef.current) {
horizontalContainerRef.current.scrollTop = scroll;
}
}, [scroll]);
return (
<div className={styles.ganttVerticalContainer}>
<svg
xmlns="http://www.w3.org/2000/svg"
width={gridProps.gridWidth}
height={calendarProps.headerHeight}
fontFamily={barProps.fontFamily}
>
<Calendar {...calendarProps} />
</svg>
<div
ref={horizontalContainerRef}
className={styles.horizontalContainer}
style={
ganttHeight
? { height: ganttHeight, width: gridProps.gridWidth }
: { width: gridProps.gridWidth }
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width={gridProps.gridWidth}
height={barProps.rowHeight * barProps.tasks.length}
fontFamily={barProps.fontFamily}
ref={ganttSVGRef}
>
<Grid {...gridProps} />
<TaskGanttContent {...newBarProps} />
</svg>
</div>
</div>
);
};

View File

@ -8,7 +8,6 @@ export type GridBodyProps = {
dates: Date[];
gridWidth: number;
rowHeight: number;
headerHeight: number;
columnWidth: number;
todayColor: string;
};
@ -16,14 +15,22 @@ export const GridBody: React.FC<GridBodyProps> = ({
tasks,
dates,
rowHeight,
headerHeight,
gridWidth,
columnWidth,
todayColor,
}) => {
let y = headerHeight;
let y = 0;
const gridRows: ReactChild[] = [];
const rowLines: ReactChild[] = [];
const rowLines: ReactChild[] = [
<line
key="RowLineFirst"
x="0"
y1={0}
x2={gridWidth}
y2={0}
className={styles.gridRowLine}
/>,
];
for (const task of tasks) {
gridRows.push(
<rect
@ -58,7 +65,7 @@ export const GridBody: React.FC<GridBodyProps> = ({
<line
key={date.getTime()}
x1={tickX}
y1={headerHeight}
y1={0}
x2={tickX}
y2={y}
className={styles.gridTick}

View File

@ -1,21 +0,0 @@
import React from "react";
import styles from "./grid.module.css";
export type GridHeaderProps = {
gridWidth: number;
headerHeight: number;
};
export const GridHeader: React.FC<GridHeaderProps> = ({
gridWidth,
headerHeight,
}) => {
return (
<rect
x="0"
y="0"
width={gridWidth}
height={headerHeight}
className={styles.gridHeader}
/>
);
};

View File

@ -1,17 +1,11 @@
.gridRow {
fill: #ffffff;
fill: #fff;
}
.gridRow:nth-child(even) {
fill: #f5f5f5;
}
.gridHeader {
fill: #ffffff;
stroke: #e0e0e0;
stroke-width: 1.4;
}
.gridRowLine {
stroke: #ebeff2;
}

View File

@ -1,12 +1,10 @@
import React from "react";
import { GridBody, GridBodyProps } from "./grid-body";
import { GridHeader, GridHeaderProps } from "./grid-header";
export type GridProps = GridBodyProps & GridHeaderProps;
export type GridProps = GridBodyProps;
export const Grid: React.FC<GridProps> = props => {
return (
<g className="grid">
<GridHeader {...props} />
<GridBody {...props} />
</g>
);

View File

@ -0,0 +1,6 @@
.scroll {
overflow: hidden auto;
margin-left: -17px;
width: 17px;
flex-shrink: 0;
}

View File

@ -0,0 +1,19 @@
import React, { SyntheticEvent } from "react";
import styles from "./scroll.module.css";
export const Scroll: React.FC<{
ganttHeight: number;
ganttFullHeight: number;
headerHeight: number;
onScroll: (event: SyntheticEvent<HTMLDivElement>) => void;
}> = ({ ganttHeight, ganttFullHeight, headerHeight, onScroll }) => {
return (
<div
style={{ height: ganttHeight, marginTop: headerHeight }}
className={styles.scroll}
onScroll={onScroll}
>
<div style={{ height: ganttFullHeight, width: 1 }} />
</div>
);
};

View File

@ -1,54 +1,63 @@
import React, { useRef, useEffect, useState } from "react";
import { Task } from "../../types/public-types";
import { BarTask } from "../../types/bar-task";
import styles from "./tooltip.module.css";
export type TooltipProps = {
x: number;
y: number;
task: Task;
svgHeight: number;
rowHeight: number;
task: BarTask;
fontSize: string;
fontFamily: string;
getTooltipContent?: (
task: Task,
fontSize: string,
fontFamily: string
) => JSX.Element;
TooltipContent: React.FC<{
task: Task;
fontSize: string;
fontFamily: string;
}>;
};
export const Tooltip: React.FC<TooltipProps> = ({
x,
y,
rowHeight,
svgHeight,
task,
fontSize,
fontFamily,
getTooltipContent = getStandardTooltipContent,
TooltipContent,
}) => {
const tooltipRef = useRef<HTMLDivElement | null>(null);
const [toolWidth, setToolWidth] = useState(1000);
const [relatedY, setRelatedY] = useState(y);
const [relatedY, setRelatedY] = useState((task.index - 1) * rowHeight);
useEffect(() => {
if (tooltipRef.current) {
const height =
tooltipRef.current.offsetHeight +
tooltipRef.current.offsetHeight * 0.15;
setRelatedY(y - height);
const tooltipHeight = tooltipRef.current.offsetHeight;
const tooltipY = task.index * rowHeight + rowHeight;
if (tooltipHeight > tooltipY) {
setRelatedY(tooltipHeight * 0.5);
} else if (tooltipY + tooltipHeight > svgHeight) {
setRelatedY(svgHeight - tooltipHeight * 1.05);
}
setToolWidth(tooltipRef.current.scrollWidth * 1.1);
}
}, [tooltipRef, y]);
}, [tooltipRef, task]);
return (
<foreignObject x={x} y={relatedY} width={toolWidth} height={1000}>
<div ref={tooltipRef} className={styles.tooltipDetailsContainer}>
{getTooltipContent(task, fontSize, fontFamily)}
<TooltipContent
task={task}
fontSize={fontSize}
fontFamily={fontFamily}
/>
</div>
</foreignObject>
);
};
const getStandardTooltipContent = (
task: Task,
fontSize: string,
fontFamily: string
) => {
export const StandardTooltipContent: React.FC<{
task: Task;
fontSize: string;
fontFamily: string;
}> = ({ task, fontSize, fontFamily }) => {
const style = {
fontSize,
fontFamily,
@ -66,9 +75,9 @@ const getStandardTooltipContent = (
(task.end.getTime() - task.start.getTime()) /
(1000 * 60 * 60 * 24)
)} day(s)`}</p>
<p
className={styles.tooltipDefaultContainerParagraph}
>{`Progress: ${task.progress} %`}</p>
<p className={styles.tooltipDefaultContainerParagraph}>
{!!task.progress && `Progress: ${task.progress} %`}
</p>
</div>
);
};

View File

@ -0,0 +1,23 @@
.ganttTable {
display: table;
border-bottom: #e6e4e4 1px solid;
border-top: #e6e4e4 1px solid;
border-left: #e6e4e4 1px solid;
}
.ganttTable_Header {
display: table-row;
list-style: none;
}
.ganttTable_HeaderSeparator {
border-right: 1px solid rgb(196, 196, 196);
opacity: 1;
margin-left: -2px;
}
.ganttTable_HeaderItem {
display: table-cell;
vertical-align: -webkit-baseline-middle;
vertical-align: middle;
}

View File

@ -0,0 +1,65 @@
import React from "react";
import styles from "./task-list-header.module.css";
export const TaskListHeaderDefault: React.SFC<{
headerHeight: number;
rowWidth: string;
fontFamily: string;
fontSize: string;
}> = ({ headerHeight, fontFamily, fontSize, rowWidth }) => {
return (
<div
className={styles.ganttTable}
style={{
fontFamily: fontFamily,
fontSize: fontSize,
}}
>
<div
className={styles.ganttTable_Header}
style={{
height: headerHeight - 2,
}}
>
<div
className={styles.ganttTable_HeaderItem}
style={{
minWidth: rowWidth,
}}
>
&nbsp;Name
</div>
<div
className={styles.ganttTable_HeaderSeparator}
style={{
height: headerHeight * 0.5,
marginTop: headerHeight * 0.2,
}}
/>
<div
className={styles.ganttTable_HeaderItem}
style={{
minWidth: rowWidth,
}}
>
&nbsp;From
</div>
<div
className={styles.ganttTable_HeaderSeparator}
style={{
height: headerHeight * 0.5,
marginTop: headerHeight * 0.25,
}}
/>
<div
className={styles.ganttTable_HeaderItem}
style={{
minWidth: rowWidth,
}}
>
&nbsp;To
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,19 @@
.taskListWrapper {
display: table;
border-bottom: #e6e4e4 1px solid;
border-left: #e6e4e4 1px solid;
}
.taskListTableRow {
display: table-row;
text-overflow: ellipsis;
}
.taskListTableRow:nth-of-type(even) {
background-color: #f5f5f5;
}
.taskListCell {
display: table-cell;
vertical-align: middle;
}

View File

@ -0,0 +1,64 @@
import React from "react";
import styles from "./task-list-table.module.css";
import { Task } from "../../types/public-types";
export const TaskListTableDefault: React.SFC<{
rowHeight: number;
rowWidth: string;
fontFamily: string;
fontSize: string;
locale: string;
tasks: Task[];
}> = ({ rowHeight, rowWidth, tasks, fontFamily, fontSize, locale }) => {
const dateTimeOptions = {
weekday: "short",
year: "numeric",
month: "long",
day: "numeric",
};
return (
<div
className={styles.taskListWrapper}
style={{
fontFamily: fontFamily,
fontSize: fontSize,
}}
>
{tasks.map(t => {
return (
<div
className={styles.taskListTableRow}
style={{ height: rowHeight }}
key={`${t.id}row`}
>
<div
className={styles.taskListCell}
style={{
minWidth: rowWidth,
maxWidth: rowWidth,
}}
>
&nbsp;{t.name}
</div>
<div
className={styles.taskListCell}
style={{
minWidth: rowWidth,
}}
>
&nbsp;{t.start.toLocaleDateString(locale, dateTimeOptions)}
</div>
<div
className={styles.taskListCell}
style={{
minWidth: rowWidth,
}}
>
&nbsp;{t.end.toLocaleDateString(locale, dateTimeOptions)}
</div>
</div>
);
})}
</div>
);
};

View File

@ -0,0 +1,78 @@
import React, { useEffect, useRef } from "react";
import { Task } from "../../types/public-types";
export type TaskListProps = {
headerHeight: number;
rowWidth: string;
fontFamily: string;
fontSize: string;
rowHeight: number;
ganttHeight: number;
scroll: number;
locale: string;
tasks: Task[];
horizontalContainerClass?: string;
TaskListHeader: React.SFC<{
headerHeight: number;
rowWidth: string;
fontFamily: string;
fontSize: string;
}>;
TaskListTable: React.SFC<{
rowHeight: number;
rowWidth: string;
fontFamily: string;
fontSize: string;
locale: string;
tasks: Task[];
}>;
};
export const TaskList: React.FC<TaskListProps> = ({
headerHeight,
fontFamily,
fontSize,
rowWidth,
rowHeight,
scroll,
tasks,
locale,
ganttHeight,
horizontalContainerClass,
TaskListHeader,
TaskListTable,
}) => {
const horizontalContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (horizontalContainerRef.current) {
horizontalContainerRef.current.scrollTop = scroll;
}
}, [scroll]);
const headerProps = {
headerHeight,
fontFamily,
fontSize,
rowWidth,
};
const tableProps = {
rowHeight,
rowWidth,
fontFamily,
fontSize,
tasks,
locale,
};
return (
<div>
<TaskListHeader {...headerProps} />
<div
ref={horizontalContainerRef}
className={horizontalContainerClass}
style={ganttHeight ? { height: ganttHeight } : {}}
>
<TaskListTable {...tableProps} />
</div>
</div>
);
};

View File

@ -7,7 +7,6 @@ export const convertToBarTasks = (
columnWidth: number,
rowHeight: number,
barFill: number,
headerHeight: number,
barCornerRadius: number,
handleWidth: number,
barProgressColor: string,
@ -31,7 +30,6 @@ export const convertToBarTasks = (
columnWidth,
rowHeight,
taskHeight,
headerHeight,
barCornerRadius,
handleWidth,
barProgressColor,
@ -64,7 +62,6 @@ export const convertToBarTask = (
columnWidth: number,
rowHeight: number,
taskHeight: number,
headerHeight: number,
barCornerRadius: number,
handleWidth: number,
barProgressColor: string,
@ -74,7 +71,7 @@ export const convertToBarTask = (
): BarTask => {
const x1 = taskXCoordinate(task.start, dates, dateDelta, columnWidth);
const x2 = taskXCoordinate(task.end, dates, dateDelta, columnWidth);
const y = taskYCoordinate(index, rowHeight, taskHeight, headerHeight);
const y = taskYCoordinate(index, rowHeight, taskHeight);
const styles = {
backgroundColor: barBackgroundColor,
@ -125,10 +122,9 @@ export const taskXCoordinate = (
export const taskYCoordinate = (
index: number,
rowHeight: number,
taskHeight: number,
headerHeight: number
taskHeight: number
) => {
const y = index * rowHeight + headerHeight + (rowHeight - taskHeight) / 2;
const y = index * rowHeight + (rowHeight - taskHeight) / 2;
return y;
};

View File

@ -1,4 +1,4 @@
export { Gantt } from "./components/Gantt/gantt";
export { Gantt } from "./components/gantt/gantt";
export { ViewMode } from "./types/public-types";
export type {
GanttProps,

View File

@ -8,6 +8,14 @@
outline: none;
}
.GanttBar-handle {
fill: #ddd;
cursor: ew-resize;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
}
.GanttBar-wrapper:hover .GanttBar-handle {
visibility: visible;
opacity: 1;
@ -31,14 +39,6 @@
text-anchor: start;
}
.GanttBar-handle {
fill: #ddd;
cursor: ew-resize;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
}
.GanttCalendar-bottomText {
text-anchor: middle;
fill: #333;
@ -66,7 +66,7 @@
}
.GanttGrid-row {
fill: #ffffff;
fill: #fff;
}
.GanttGrid-row:nth-child(even) {
@ -74,7 +74,7 @@
}
.GanttGrid-header {
fill: #ffffff;
fill: #fff;
stroke: #e0e0e0;
stroke-width: 1.4;
}

View File

@ -47,7 +47,9 @@ export interface DisplayOption {
export interface StylingOption {
headerHeight?: number;
columnWidth?: number;
listCellWidth?: string;
rowHeight?: number;
ganttHeight?: number;
barCornerRadius?: number;
handleWidth?: number;
fontFamily?: string;
@ -64,11 +66,25 @@ export interface StylingOption {
arrowColor?: string;
arrowIndent?: number;
todayColor?: string;
getTooltipContent?: (
task: Task,
fontSize: string,
fontFamily: string
) => JSX.Element;
TooltipContent?: React.FC<{
task: Task;
fontSize: string;
fontFamily: string;
}>;
TaskListHeader?: React.SFC<{
headerHeight: number;
rowWidth: string;
fontFamily: string;
fontSize: string;
}>;
TaskListTable?: React.SFC<{
rowHeight: number;
rowWidth: string;
fontFamily: string;
fontSize: string;
locale: string;
tasks: Task[];
}>;
}
export interface GanttProps extends EventOption, DisplayOption, StylingOption {