diff --git a/example/src/App.tsx b/example/src/App.tsx
index 9d8788e..63e9a81 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -79,16 +79,20 @@ const App = () => {
},
];
+ const sleep = (milliseconds: number) => {
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
+ };
let onTaskChange = (task: Task) => {
console.log("On date change Id:" + task.id);
};
let onTaskDelete = (task: Task) => {
- const conf = window.confirm("Are you sure?");
- if (!conf) throw "No del Id:" + task.id;
+ const conf = window.confirm("Are you sure about " + task.name + " ?");
+ return conf;
};
- let onProgressChange = (task: Task) => {
+ let onProgressChange = async (task: Task) => {
+ await sleep(5000);
console.log("On progress change Id:" + task.id);
};
@@ -96,6 +100,10 @@ const App = () => {
alert("On Double Click event Id:" + task.id);
};
+ let onSelect = (task: Task, isSelected: boolean) => {
+ console.log(task.name + " has " + (isSelected ? "selected" : "unselected"));
+ };
+
return (
{
onTaskDelete={onTaskDelete}
onProgressChange={onProgressChange}
onDoubleClick={onDblClick}
+ onSelect={onSelect}
listCellWidth={isChecked ? "155px" : ""}
columnWidth={columnWidth}
/>
@@ -122,6 +131,7 @@ const App = () => {
onTaskDelete={onTaskDelete}
onProgressChange={onProgressChange}
onDoubleClick={onDblClick}
+ onSelect={onSelect}
listCellWidth={isChecked ? "155px" : ""}
ganttHeight={300}
columnWidth={columnWidth}
diff --git a/package.json b/package.json
index ce43c2f..ddd0b30 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gantt-task-react",
- "version": "0.1.8",
+ "version": "0.1.9",
"description": "Interactive Gantt Chart for React with TypeScript.",
"author": "MaTeMaTuK ",
"homepage": "https://github.com/MaTeMaTuK/gantt-task-react",
diff --git a/src/components/bar/bar.tsx b/src/components/bar/bar.tsx
index 61170f8..11291de 100644
--- a/src/components/bar/bar.tsx
+++ b/src/components/bar/bar.tsx
@@ -17,7 +17,7 @@ export type BarProps = {
isDateChangeable: boolean;
isDelete: boolean;
onEventStart: (
- event: React.MouseEvent | React.KeyboardEvent,
+ event: React.MouseEvent | React.KeyboardEvent | React.FocusEvent,
action: GanttContentMoveAction,
selectedTask: BarTask
) => any;
@@ -43,9 +43,6 @@ export const Bar: React.FC = ({
return (
{
- onEventStart(e, "dblclick", task);
- }}
tabIndex={0}
onKeyDown={e => {
switch (e.key) {
@@ -62,8 +59,17 @@ export const Bar: React.FC = ({
onMouseLeave={e => {
onEventStart(e, "mouseleave", task);
}}
- onFocus={() => setIsSelected(true)}
- onBlur={() => setIsSelected(false)}
+ onDoubleClick={e => {
+ onEventStart(e, "dblclick", task);
+ }}
+ onFocus={e => {
+ setIsSelected(true);
+ onEventStart(e, "select", task);
+ }}
+ onBlur={e => {
+ setIsSelected(false);
+ onEventStart(e, "unselect", task);
+ }}
>
= ({
onProgressChange,
onDoubleClick,
onTaskDelete,
+ onSelect,
}) => {
const wrapperRef = useRef(null);
const [ganttTasks, setGanttTasks] = useState(tasks);
@@ -98,8 +99,10 @@ export const Gantt: React.SFC = ({
};
const handleScrollX = (event: SyntheticEvent) => {
- if (scrollX !== event.currentTarget.scrollLeft)
+ if (scrollX !== event.currentTarget.scrollLeft && !ignoreScrollEvent) {
setScrollX(event.currentTarget.scrollLeft);
+ }
+ setIgnoreScrollEvent(false);
};
/**
@@ -195,6 +198,7 @@ export const Gantt: React.SFC = ({
onProgressChange,
onDoubleClick,
onTaskDelete,
+ onSelect,
TooltipContent,
};
diff --git a/src/components/gantt/task-gantt-content.tsx b/src/components/gantt/task-gantt-content.tsx
index 84a6ff1..e820b6e 100644
--- a/src/components/gantt/task-gantt-content.tsx
+++ b/src/components/gantt/task-gantt-content.tsx
@@ -9,13 +9,15 @@ import {
BarMoveAction,
} from "../../helpers/bar-helper";
import { Tooltip } from "../other/tooltip";
-import { isKeyboardEvent } from "../../helpers/other-helper";
+import { isKeyboardEvent, isMouseEvent } from "../../helpers/other-helper";
export type GanttContentMoveAction =
| "mouseenter"
| "mouseleave"
| "delete"
| "dblclick"
+ | "select"
+ | "unselect"
| BarMoveAction;
export type BarEvent = {
selectedTask?: BarTask;
@@ -73,6 +75,7 @@ export const TaskGanttContent: React.FC = ({
onProgressChange,
onDoubleClick,
onTaskDelete,
+ onSelect,
TooltipContent,
}) => {
const point = svg?.current?.createSVGPoint();
@@ -80,6 +83,7 @@ export const TaskGanttContent: React.FC = ({
action: "",
});
const [barTasks, setBarTasks] = useState([]);
+ const [failedTask, setFailedTask] = useState(null);
const [xStep, setXStep] = useState(0);
const [initEventX1Delta, setInitEventX1Delta] = useState(0);
const [isMoving, setIsMoving] = useState(false);
@@ -126,48 +130,16 @@ export const TaskGanttContent: React.FC = ({
barBackgroundSelectedColor,
]);
- /**
- * Method is Start point of task change
- */
- const handleBarEventStart = async (
- event: React.MouseEvent | React.KeyboardEvent,
- action: GanttContentMoveAction,
- selectedTask: BarTask
- ) => {
- if (isKeyboardEvent(event)) {
- if (action === "delete") {
- if (onTaskDelete) {
- await onTaskDelete(selectedTask);
- const newTasks = barTasks.filter(t => t.id !== selectedTask.id);
- onTasksChange(newTasks);
- }
- }
- } else if (action === "mouseenter") {
- if (!barEvent.action) {
- setBarEvent({ action, selectedTask, originalTask: selectedTask });
- }
- } else if (action === "mouseleave") {
- if (barEvent.action === "mouseenter") {
- setBarEvent({ action: "" });
- }
- } else if (action === "move") {
- if (!svg?.current || !point) return;
- point.x = event.clientX;
- const cursor = point.matrixTransform(
- svg.current.getScreenCTM()?.inverse()
+ // on failed task update
+ useEffect(() => {
+ if (failedTask) {
+ const newTasks = barTasks.map(t =>
+ t.id === failedTask.id ? failedTask : t
);
- setInitEventX1Delta(cursor.x - selectedTask.x1);
- setBarEvent({ action, selectedTask, originalTask: selectedTask });
- } else if (action === "dblclick") {
- !!onDoubleClick && onDoubleClick(selectedTask);
- } else {
- setBarEvent({
- action,
- selectedTask,
- originalTask: selectedTask,
- });
+ onTasksChange(newTasks);
+ setFailedTask(null);
}
- };
+ }, [failedTask, barTasks]);
useEffect(() => {
const handleMouseMove = async (event: MouseEvent) => {
@@ -220,25 +192,46 @@ export const TaskGanttContent: React.FC = ({
originalTask.end !== changedTask.end ||
originalTask.progress !== changedTask.progress;
- if (
- (action === "move" || action === "end" || action === "start") &&
- onDateChange &&
- isNotLikeOriginal
- ) {
- await onDateChange(changedTask);
- } else if (onProgressChange && isNotLikeOriginal) {
- await onProgressChange(changedTask);
- }
-
+ // remove listeners
+ svg.current.removeEventListener("mousemove", handleMouseMove);
+ svg.current.removeEventListener("mouseup", handleMouseUp);
+ setBarEvent({ action: "" });
+ setIsMoving(false);
const newTasks = barTasks.map(t =>
t.id === changedTask.id ? changedTask : t
);
onTasksChange(newTasks);
- svg.current.removeEventListener("mousemove", handleMouseMove);
- svg.current.removeEventListener("mouseup", handleMouseUp);
- setBarEvent({ action: "" });
- setIsMoving(false);
+ // custom operation start
+ let operationSuccess = true;
+ if (
+ (action === "move" || action === "end" || action === "start") &&
+ onDateChange &&
+ isNotLikeOriginal
+ ) {
+ try {
+ const result = await onDateChange(changedTask);
+ if (result !== undefined) {
+ operationSuccess = result;
+ }
+ } catch (error) {
+ operationSuccess = false;
+ }
+ } else if (onProgressChange && isNotLikeOriginal) {
+ try {
+ const result = await onProgressChange(changedTask);
+ if (result !== undefined) {
+ operationSuccess = result;
+ }
+ } catch (error) {
+ operationSuccess = false;
+ }
+ }
+
+ // If operation is failed - return old state
+ if (!operationSuccess) {
+ setFailedTask(originalTask);
+ }
};
if (
@@ -265,6 +258,67 @@ export const TaskGanttContent: React.FC = ({
isMoving,
]);
+ /**
+ * Method is Start point of task change
+ */
+ const handleBarEventStart = async (
+ event: React.MouseEvent | React.KeyboardEvent | React.FocusEvent,
+ action: GanttContentMoveAction,
+ selectedTask: BarTask
+ ) => {
+ // Keyboard events
+ if (isKeyboardEvent(event)) {
+ if (action === "delete") {
+ if (onTaskDelete) {
+ try {
+ const result = await onTaskDelete(selectedTask);
+ if (result !== undefined && result) {
+ const newTasks = barTasks.filter(t => t.id !== selectedTask.id);
+ onTasksChange(newTasks);
+ !!onSelect && onSelect(selectedTask, false);
+ }
+ } catch (error) {
+ console.error("Error on Delete. " + error);
+ }
+ }
+ }
+ } else if (!isMouseEvent(event)) {
+ if (action === "select") {
+ !!onSelect && onSelect(selectedTask, true);
+ } else if (action === "unselect") {
+ !!onSelect && onSelect(selectedTask, false);
+ }
+ }
+ // Mouse Events
+ else if (action === "mouseenter") {
+ if (!barEvent.action) {
+ setBarEvent({ action, selectedTask, originalTask: selectedTask });
+ }
+ } else if (action === "mouseleave") {
+ if (barEvent.action === "mouseenter") {
+ setBarEvent({ action: "" });
+ }
+ } else if (action === "dblclick") {
+ !!onDoubleClick && onDoubleClick(selectedTask);
+ }
+ // Change task event start
+ else if (action === "move") {
+ if (!svg?.current || !point) return;
+ point.x = event.clientX;
+ const cursor = point.matrixTransform(
+ svg.current.getScreenCTM()?.inverse()
+ );
+ setInitEventX1Delta(cursor.x - selectedTask.x1);
+ setBarEvent({ action, selectedTask, originalTask: selectedTask });
+ } else {
+ setBarEvent({
+ action,
+ selectedTask,
+ originalTask: selectedTask,
+ });
+ }
+ };
+
return (
diff --git a/src/helpers/other-helper.ts b/src/helpers/other-helper.ts
index 2f27bf5..d45939d 100644
--- a/src/helpers/other-helper.ts
+++ b/src/helpers/other-helper.ts
@@ -2,11 +2,17 @@ import { BarTask } from "../types/bar-task";
import { Task } from "../types/public-types";
export function isKeyboardEvent(
- event: React.MouseEvent | React.KeyboardEvent
+ event: React.MouseEvent | React.KeyboardEvent | React.FocusEvent
): event is React.KeyboardEvent {
return (event as React.KeyboardEvent).key !== undefined;
}
+export function isMouseEvent(
+ event: React.MouseEvent | React.KeyboardEvent | React.FocusEvent
+): event is React.MouseEvent {
+ return (event as React.MouseEvent).clientX !== undefined;
+}
+
export function isBarTask(task: Task | BarTask): task is BarTask {
return (task as BarTask).x1 !== undefined;
}
diff --git a/src/types/public-types.ts b/src/types/public-types.ts
index 4201adc..7ce9b55 100644
--- a/src/types/public-types.ts
+++ b/src/types/public-types.ts
@@ -30,10 +30,32 @@ export interface EventOption {
* Time step value for date changes.
*/
timeStep?: number;
+ /**
+ * Invokes on bar select on unselect.
+ */
+ onSelect?: (task: Task, isSelected: boolean) => void;
+ /**
+ * Invokes on bar double click.
+ */
onDoubleClick?: (task: Task) => void;
- onDateChange?: (task: Task) => void | Promise;
- onProgressChange?: (task: Task) => void | Promise;
- onTaskDelete?: (task: Task) => void | Promise;
+ /**
+ * Invokes on end and start time change. Chart undoes operation if method return false or error.
+ */
+ onDateChange?: (
+ task: Task
+ ) => void | boolean | Promise | Promise;
+ /**
+ * Invokes on progress change. Chart undoes operation if method return false or error.
+ */
+ onProgressChange?: (
+ task: Task
+ ) => void | boolean | Promise | Promise;
+ /**
+ * Invokes on delete selected task. Chart undoes operation if method return false or error.
+ */
+ onTaskDelete?: (
+ task: Task
+ ) => void | boolean | Promise | Promise;
}
export interface DisplayOption {