From 270131067cdd3187a9a68d44e2c8e2676470ac2a Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 9 Sep 2020 17:08:16 +0300 Subject: [PATCH] add onSelect event & change and delete event fix --- example/src/App.tsx | 16 +- package.json | 2 +- src/components/bar/bar.tsx | 18 ++- src/components/gantt/gantt.tsx | 6 +- src/components/gantt/task-gantt-content.tsx | 164 +++++++++++++------- src/helpers/other-helper.ts | 8 +- src/types/public-types.ts | 28 +++- 7 files changed, 172 insertions(+), 70 deletions(-) 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 {