From 7833e05da551c04017f02f4e511896a6c28cd537 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 9 Aug 2020 10:51:25 +0300 Subject: [PATCH] event handle rework + firefox support --- example/src/App.tsx | 4 +- src/components/Bar/bar.tsx | 43 ++- src/components/Gantt/gantt-content.tsx | 430 +++++++++---------------- src/components/Gantt/gantt.tsx | 7 +- src/helpers/bar-helper.ts | 82 +++++ src/helpers/other-helper.ts | 5 + 6 files changed, 259 insertions(+), 312 deletions(-) create mode 100644 src/helpers/other-helper.ts diff --git a/example/src/App.tsx b/example/src/App.tsx index 1f00f87..b14f529 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -57,8 +57,8 @@ const App = () => { }, { start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15), - end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 26), - name: "Release", + end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 16), + name: "Release & Eat Burgers", id: "Task 6", progress: currentDate.getMonth(), dependencies: ["Task 4"], diff --git a/src/components/Bar/bar.tsx b/src/components/Bar/bar.tsx index fe90d2f..7f2eb7f 100644 --- a/src/components/Bar/bar.tsx +++ b/src/components/Bar/bar.tsx @@ -4,12 +4,12 @@ import { BarProgressHandle } from "./bar-progress-handle"; import { BarDateHandle } from "./bar-date-handle"; import { BarDisplay } from "./bar-display"; import { BarTask } from "../../types/bar-task"; -import { BarAction } from "../Gantt/gantt-content"; import { progressWithByParams, getProgressPoint, } from "../../helpers/bar-helper"; import styles from "./bar.module.css"; +import { GanttContentMoveAction } from "../Gantt/gantt-content"; export type BarProps = { task: BarTask; @@ -17,18 +17,12 @@ export type BarProps = { onDoubleClick?: (task: Task) => any; isProgressChangeable: boolean; isDateChangeable: boolean; - handleMouseEvents: ( - event: - | React.MouseEvent - | React.MouseEvent - | React.MouseEvent, - eventType: BarAction, - task: BarTask - ) => void; - handleButtonSVGEvents: ( - event: React.KeyboardEvent, - task: BarTask - ) => void; + isDelete: boolean; + onEventStart: ( + event: React.MouseEvent | React.KeyboardEvent, + action: GanttContentMoveAction, + selectedTask: BarTask + ) => any; }; export const Bar: React.FC = ({ @@ -37,8 +31,8 @@ export const Bar: React.FC = ({ onDoubleClick, isProgressChangeable, isDateChangeable, - handleMouseEvents, - handleButtonSVGEvents, + onEventStart, + isDelete, }) => { const [isSelected, setIsSelected] = useState(false); @@ -57,13 +51,18 @@ export const Bar: React.FC = ({ }} tabIndex={0} onKeyDown={e => { - handleButtonSVGEvents(e, task); + switch (e.key) { + case "Delete": { + if (isDelete) onEventStart(e, "delete", task); + break; + } + } }} onMouseEnter={e => { - handleMouseEvents(e, "mouseenter", task); + onEventStart(e, "mouseenter", task); }} onMouseLeave={e => { - handleMouseEvents(e, "mouseleave", task); + onEventStart(e, "mouseleave", task); }} onFocus={() => setIsSelected(true)} onBlur={() => setIsSelected(false)} @@ -81,7 +80,7 @@ export const Bar: React.FC = ({ styles={task.styles} isSelected={isSelected} onMouseDown={e => { - isDateChangeable && handleMouseEvents(e, "move", task); + isDateChangeable && onEventStart(e, "move", task); }} /> @@ -95,7 +94,7 @@ export const Bar: React.FC = ({ height={task.height - 2} barCornerRadius={task.barCornerRadius} onMouseDown={e => { - handleMouseEvents(e, "start", task); + onEventStart(e, "start", task); }} /> {/* right */} @@ -106,7 +105,7 @@ export const Bar: React.FC = ({ height={task.height - 2} barCornerRadius={task.barCornerRadius} onMouseDown={e => { - handleMouseEvents(e, "end", task); + onEventStart(e, "end", task); }} /> @@ -115,7 +114,7 @@ export const Bar: React.FC = ({ { - handleMouseEvents(e, "progress", task); + onEventStart(e, "progress", task); }} /> )} diff --git a/src/components/Gantt/gantt-content.tsx b/src/components/Gantt/gantt-content.tsx index 94a20e7..4e435b9 100644 --- a/src/components/Gantt/gantt-content.tsx +++ b/src/components/Gantt/gantt-content.tsx @@ -1,25 +1,25 @@ -import React, { useState, useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Task, EventOption } from "../../types/public-types"; import { Bar } from "../Bar/bar"; import { BarTask } from "../../types/bar-task"; import { Arrow } from "../Other/arrow"; import { convertToBarTasks, - progressByX, - startByX, - endByX, - moveByX, - dateByX, + handleTaskBySVGMouseEvent, + BarMoveAction, } from "../../helpers/bar-helper"; import { Tooltip } from "../Other/tooltip"; +import { isKeyboardEvent } from "../../helpers/other-helper"; -export interface GanttTask extends Task { - x1: number; - x2: number; - y: number; - width: number; - height: number; -} +export type GanttContentMoveAction = + | "mouseenter" + | "mouseleave" + | "delete" + | BarMoveAction; +export type BarEvent = { + selectedTask?: BarTask; + action: GanttContentMoveAction; +}; export type GanttContentProps = { tasks: Task[]; dates: Date[]; @@ -33,8 +33,8 @@ export type GanttContentProps = { barBackgroundSelectedColor: string; headerHeight: number; handleWidth: number; - svg: React.MutableRefObject; timeStep: number; + svg: React.RefObject; arrowColor: string; arrowIndent: number; fontSize: string; @@ -46,19 +46,6 @@ export type GanttContentProps = { ) => JSX.Element; } & EventOption; -export type BarAction = - | "progress" - | "end" - | "start" - | "move" - | "mouseenter" - | "mouseleave" - | ""; - -type BarEvent = { - action: BarAction; - selectedTask: BarTask | null; -}; export const GanttContent: React.FC = ({ tasks, rowHeight, @@ -73,26 +60,26 @@ export const GanttContent: React.FC = ({ headerHeight, handleWidth, arrowColor, - svg, timeStep, fontFamily, fontSize, arrowIndent, + svg, onDateChange, onProgressChange, onDoubleClick, onTaskDelete, getTooltipContent, }) => { + const point = svg.current?.createSVGPoint(); const [barEvent, setBarEvent] = useState({ action: "", - selectedTask: null, }); - const [isSVGListen, setIsSVGListen] = useState(false); const [barTasks, setBarTasks] = useState([]); const [xStep, setXStep] = useState(0); const [initEventX1Delta, setInitEventX1Delta] = useState(0); - + const [isMoving, setIsMoving] = useState(false); + // create xStep useEffect(() => { const dateDelta = dates[1].getTime() - @@ -103,8 +90,9 @@ export const GanttContent: React.FC = ({ if (newXStep !== xStep) { setXStep(newXStep); } - }, [tasks, rowHeight, barCornerRadius, columnWidth, dates, timeStep, xStep]); + }, [columnWidth, dates, timeStep, xStep]); + // generate tasks useEffect(() => { const dateDelta = dates[1].getTime() - @@ -136,257 +124,133 @@ export const GanttContent: React.FC = ({ barCornerRadius, columnWidth, dates, - timeStep, barFill, handleWidth, headerHeight, - ]); - - useEffect(() => { - /** - * Method handles event in real time(mousemove) and on finish(mouseup) - */ - const handleMouseSVGChangeEventsSubscribe = async (event: MouseEvent) => { - if (!barEvent.selectedTask || !barEvent.action || !svg || !svg.current) - return; - - const selectedTask = barEvent.selectedTask; - const changedTask = { ...selectedTask } as BarTask; - switch (event.type) { - // On Event changing - case "mousemove": { - switch (barEvent.action) { - case "progress": - changedTask.progress = progressByX(event.offsetX, selectedTask); - break; - case "start": { - const newX1 = startByX(event.offsetX, xStep, selectedTask); - changedTask.x1 = newX1; - changedTask.start = dateByX( - newX1, - selectedTask.x1, - selectedTask.start, - xStep, - timeStep - ); - break; - } - case "end": { - const newX2 = endByX(event.offsetX, xStep, selectedTask); - changedTask.x2 = newX2; - changedTask.end = dateByX( - newX2, - selectedTask.x2, - selectedTask.end, - xStep, - timeStep - ); - break; - } - case "move": { - const [newMoveX1, newMoveX2] = moveByX( - event.offsetX - initEventX1Delta, - xStep, - selectedTask - ); - changedTask.start = dateByX( - newMoveX1, - selectedTask.x1, - selectedTask.start, - xStep, - timeStep - ); - changedTask.end = dateByX( - newMoveX2, - selectedTask.x2, - selectedTask.end, - xStep, - timeStep - ); - changedTask.x1 = newMoveX1; - changedTask.x2 = newMoveX2; - break; - } - } - - // Update internal state - setBarTasks( - barTasks.map(t => (t.id === changedTask.id ? changedTask : t)) - ); - setBarEvent({ ...barEvent, selectedTask: changedTask }); - break; - } - - // On finish Event - case "mouseup": { - let eventForExecution: ( - task: Task - ) => void | Promise = () => {}; - switch (barEvent.action) { - case "progress": - changedTask.progress = progressByX(event.offsetX, selectedTask); - if (onProgressChange) { - eventForExecution = onProgressChange; - } - break; - case "start": { - const newX1 = startByX(event.offsetX, xStep, selectedTask); - changedTask.start = dateByX( - newX1, - selectedTask.x1, - selectedTask.start, - xStep, - timeStep - ); - if (onDateChange && newX1 !== selectedTask.x1) { - eventForExecution = onDateChange; - } - break; - } - case "end": { - const newX2 = endByX(event.offsetX, xStep, selectedTask); - changedTask.end = dateByX( - newX2, - selectedTask.x2, - selectedTask.end, - xStep, - timeStep - ); - - if (onDateChange && newX2 !== selectedTask.x2) { - eventForExecution = onDateChange; - } - break; - } - case "move": { - const [newMoveX1, newMoveX2] = moveByX( - event.offsetX - initEventX1Delta, - xStep, - selectedTask - ); - changedTask.start = dateByX( - newMoveX1, - selectedTask.x1, - selectedTask.start, - xStep, - timeStep - ); - changedTask.end = dateByX( - newMoveX2, - selectedTask.x2, - selectedTask.end, - xStep, - timeStep - ); - if ( - onDateChange && - newMoveX1 !== selectedTask.x1 && - newMoveX2 !== selectedTask.x2 - ) { - eventForExecution = onDateChange; - } - break; - } - } - - setBarEvent({ action: "", selectedTask: null }); - setIsSVGListen(false); - svg.current.removeEventListener( - "mousemove", - handleMouseSVGChangeEventsSubscribe - ); - svg.current.removeEventListener( - "mouseup", - handleMouseSVGChangeEventsSubscribe - ); - - // If update successful - update Gantt state, otherwise we shell back old Bar state - await eventForExecution(changedTask); - break; - } - } - }; - - if ( - barEvent.selectedTask && - barEvent.action && - !isSVGListen && - svg && - svg.current - ) { - setIsSVGListen(true); - svg.current.addEventListener( - "mousemove", - handleMouseSVGChangeEventsSubscribe - ); - svg.current.addEventListener( - "mouseup", - handleMouseSVGChangeEventsSubscribe - ); - } - }, [ - barEvent, - isSVGListen, - xStep, - svg, - initEventX1Delta, - barTasks, - onProgressChange, - timeStep, - onDateChange, + barProgressColor, + barProgressSelectedColor, + barBackgroundColor, + barBackgroundSelectedColor, ]); /** * Method is Start point of task change - * @param event init mouse event - * @param eventType - * @param task events task */ - const handleMouseEvents = ( - event: - | React.MouseEvent - | React.MouseEvent - | React.MouseEvent, - eventType: BarAction, - task: BarTask + const handleBarEventStart = ( + event: React.MouseEvent | React.KeyboardEvent, + action: GanttContentMoveAction, + selectedTask: BarTask ) => { - switch (event.type) { - case "mousedown": - setBarEvent({ ...barEvent, selectedTask: task, action: eventType }); - setInitEventX1Delta(event.nativeEvent.offsetX - task.x1); - event.stopPropagation(); - break; - case "mouseleave": - if (!barEvent.action) - setBarEvent({ ...barEvent, selectedTask: null, action: "" }); - break; - case "mouseenter": - if (!barEvent.selectedTask) { - setBarEvent({ ...barEvent, selectedTask: task, action: "" }); - } - break; + if (isKeyboardEvent(event)) { + if (action === "delete") { + setBarTasks(barTasks.filter(t => t.id !== barEvent.selectedTask?.id)); + } + } else if (action === "mouseenter") { + if (!barEvent.action) { + setBarEvent({ action, 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() + ); + setInitEventX1Delta(cursor.x - selectedTask.x1); + setBarEvent({ action, selectedTask }); + } else { + setBarEvent({ + action, + selectedTask, + }); } }; - /** - * Method handles Bar keyboard events - * @param event - * @param task - */ - const handleButtonSVGEvents = async ( - event: React.KeyboardEvent, - task: BarTask - ) => { - if (task.isDisabled) return; - switch (event.key) { - case "Delete": { - if (onTaskDelete) { - onTaskDelete(task); - } - break; + useEffect(() => { + const handleMouseMove = async (event: MouseEvent) => { + if (!barEvent.selectedTask || !point || !svg.current) return; + event.preventDefault(); + + point.x = event.clientX; + const cursor = point.matrixTransform( + svg.current.getScreenCTM()?.inverse() + ); + + const { isChanged, changedTask } = handleTaskBySVGMouseEvent( + cursor.x, + barEvent.action as BarMoveAction, + barEvent.selectedTask, + xStep, + timeStep, + initEventX1Delta + ); + if (isChanged) { + setBarTasks( + barTasks.map(t => (t.id === changedTask.id ? changedTask : t)) + ); + setBarEvent({ ...barEvent, selectedTask: changedTask }); } + }; + + const handleMouseUp = async (event: MouseEvent) => { + const { selectedTask, action } = barEvent; + if (!selectedTask || !point || !svg.current) return; + event.preventDefault(); + + point.x = event.clientX; + const cursor = point.matrixTransform( + svg.current.getScreenCTM()?.inverse() + ); + + const { changedTask } = handleTaskBySVGMouseEvent( + cursor.x, + action as BarMoveAction, + selectedTask, + xStep, + timeStep, + initEventX1Delta + ); + if ( + (action === "move" || action === "end" || action === "start") && + onDateChange + ) { + onDateChange(changedTask); + } else if (onProgressChange) { + onProgressChange(changedTask); + } + svg.current.removeEventListener("mousemove", handleMouseMove); + svg.current.removeEventListener("mouseup", handleMouseUp); + console.log(`Start: ${changedTask.start} End: ${changedTask.end}`); + setBarEvent({ action: "" }); + setIsMoving(false); + }; + + if ( + !isMoving && + (barEvent.action === "move" || + barEvent.action === "end" || + barEvent.action === "start" || + barEvent.action === "progress") && + svg.current + ) { + svg.current.addEventListener("mousemove", handleMouseMove); + svg.current.addEventListener("mouseup", handleMouseUp); + setIsMoving(true); } - }; + }, [ + barTasks, + barEvent, + xStep, + initEventX1Delta, + onProgressChange, + timeStep, + onDateChange, + svg, + isMoving, + ]); return ( @@ -395,7 +259,7 @@ export const GanttContent: React.FC = ({ return task.barChildren.map(child => { return ( = ({ isProgressChangeable={!!onProgressChange && !task.isDisabled} onDoubleClick={onDoubleClick} isDateChangeable={!!onDateChange && !task.isDisabled} - handleMouseEvents={handleMouseEvents} - handleButtonSVGEvents={handleButtonSVGEvents} + isDelete={!!onTaskDelete && !task.isDisabled} + onEventStart={handleBarEventStart} key={task.id} /> ); })} - {barEvent.selectedTask && - barEvent.action !== "end" && - barEvent.action !== "start" && ( - - )} + {barEvent.selectedTask && ( + + )} ); diff --git a/src/components/Gantt/gantt.tsx b/src/components/Gantt/gantt.tsx index 2e1bad7..9b065d7 100644 --- a/src/components/Gantt/gantt.tsx +++ b/src/components/Gantt/gantt.tsx @@ -33,7 +33,7 @@ export const Gantt: React.SFC = ({ }) => { const [startDate, endDate] = ganttDateRange(tasks, viewMode); const dates = seedDates(startDate, endDate, viewMode); - const svg = useRef(null); + const svg = useRef(null); const gridProps: GridProps = { columnWidth, @@ -68,24 +68,23 @@ export const Gantt: React.SFC = ({ handleWidth, timeStep, arrowColor, - svg, fontFamily, fontSize, arrowIndent, + svg, onDateChange, onProgressChange, onDoubleClick, onTaskDelete, getTooltipContent, }; - return ( diff --git a/src/helpers/bar-helper.ts b/src/helpers/bar-helper.ts index 300b590..e1bc2ee 100644 --- a/src/helpers/bar-helper.ts +++ b/src/helpers/bar-helper.ts @@ -68,6 +68,7 @@ export const convertToBarTask = ( const x1 = taskXCoordinate(task.start, dates, dateDelta, columnWidth); const x2 = taskXCoordinate(task.end, dates, dateDelta, columnWidth); const y = taskYCoordinate(index, rowHeight, taskHeight, headerHeight); + const styles = { backgroundColor: barBackgroundColor, backgroundSelectedColor: barBackgroundSelectedColor, @@ -213,3 +214,84 @@ export const dateByX = ( ); return newDate; }; + +export type BarMoveAction = "progress" | "end" | "start" | "move" | ""; + +/** + * Method handles event in real time(mousemove) and on finish(mouseup) + */ +export const handleTaskBySVGMouseEvent = ( + svgX: number, + action: BarMoveAction, + selectedTask: BarTask, + xStep: number, + timeStep: number, + initEventX1Delta: number +) => { + const changedTask: BarTask = { ...selectedTask }; + let isChanged = false; + switch (action) { + case "progress": + changedTask.progress = progressByX(svgX, selectedTask); + isChanged = changedTask.progress !== selectedTask.progress; + break; + case "start": { + const newX1 = startByX(svgX, xStep, selectedTask); + changedTask.x1 = newX1; + isChanged = changedTask.x1 !== selectedTask.x1; + if (isChanged) { + changedTask.start = dateByX( + newX1, + selectedTask.x1, + selectedTask.start, + xStep, + timeStep + ); + } + break; + } + case "end": { + const newX2 = endByX(svgX, xStep, selectedTask); + changedTask.x2 = newX2; + isChanged = changedTask.x2 !== selectedTask.x2; + if (isChanged) { + changedTask.end = dateByX( + newX2, + selectedTask.x2, + selectedTask.end, + xStep, + timeStep + ); + } + break; + } + 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 = dateByX( + newMoveX2, + selectedTask.x2, + selectedTask.end, + xStep, + timeStep + ); + changedTask.x1 = newMoveX1; + changedTask.x2 = newMoveX2; + } + break; + } + } + return { isChanged, changedTask }; +}; diff --git a/src/helpers/other-helper.ts b/src/helpers/other-helper.ts new file mode 100644 index 0000000..36d226e --- /dev/null +++ b/src/helpers/other-helper.ts @@ -0,0 +1,5 @@ +export function isKeyboardEvent( + event: React.MouseEvent | React.KeyboardEvent +): event is React.KeyboardEvent { + return (event as React.KeyboardEvent).key !== undefined; +}