event handle rework + firefox support

This commit is contained in:
unknown 2020-08-09 10:51:25 +03:00
parent 39f2188c76
commit 7833e05da5
6 changed files with 259 additions and 312 deletions

View File

@ -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"],

View File

@ -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<SVGPolygonElement, MouseEvent>
| React.MouseEvent<SVGGElement, MouseEvent>
| React.MouseEvent<SVGRectElement, MouseEvent>,
eventType: BarAction,
task: BarTask
) => void;
handleButtonSVGEvents: (
event: React.KeyboardEvent<SVGGElement>,
task: BarTask
) => void;
isDelete: boolean;
onEventStart: (
event: React.MouseEvent | React.KeyboardEvent,
action: GanttContentMoveAction,
selectedTask: BarTask
) => any;
};
export const Bar: React.FC<BarProps> = ({
@ -37,8 +31,8 @@ export const Bar: React.FC<BarProps> = ({
onDoubleClick,
isProgressChangeable,
isDateChangeable,
handleMouseEvents,
handleButtonSVGEvents,
onEventStart,
isDelete,
}) => {
const [isSelected, setIsSelected] = useState(false);
@ -57,13 +51,18 @@ export const Bar: React.FC<BarProps> = ({
}}
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<BarProps> = ({
styles={task.styles}
isSelected={isSelected}
onMouseDown={e => {
isDateChangeable && handleMouseEvents(e, "move", task);
isDateChangeable && onEventStart(e, "move", task);
}}
/>
<g className="handleGroup">
@ -95,7 +94,7 @@ export const Bar: React.FC<BarProps> = ({
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<BarProps> = ({
height={task.height - 2}
barCornerRadius={task.barCornerRadius}
onMouseDown={e => {
handleMouseEvents(e, "end", task);
onEventStart(e, "end", task);
}}
/>
</g>
@ -115,7 +114,7 @@ export const Bar: React.FC<BarProps> = ({
<BarProgressHandle
progressPoint={progressPoint}
onMouseDown={e => {
handleMouseEvents(e, "progress", task);
onEventStart(e, "progress", task);
}}
/>
)}

View File

@ -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<SVGSVGElement | null>;
timeStep: number;
svg: React.RefObject<SVGSVGElement>;
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<GanttContentProps> = ({
tasks,
rowHeight,
@ -73,26 +60,26 @@ export const GanttContent: React.FC<GanttContentProps> = ({
headerHeight,
handleWidth,
arrowColor,
svg,
timeStep,
fontFamily,
fontSize,
arrowIndent,
svg,
onDateChange,
onProgressChange,
onDoubleClick,
onTaskDelete,
getTooltipContent,
}) => {
const point = svg.current?.createSVGPoint();
const [barEvent, setBarEvent] = useState<BarEvent>({
action: "",
selectedTask: null,
});
const [isSVGListen, setIsSVGListen] = useState(false);
const [barTasks, setBarTasks] = useState<BarTask[]>([]);
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<GanttContentProps> = ({
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,258 +124,134 @@ export const GanttContent: React.FC<GanttContentProps> = ({
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<void> = () => {};
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<SVGPolygonElement, MouseEvent>
| React.MouseEvent<SVGRectElement, MouseEvent>
| React.MouseEvent<SVGGElement, 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: "" });
if (isKeyboardEvent(event)) {
if (action === "delete") {
setBarTasks(barTasks.filter(t => t.id !== barEvent.selectedTask?.id));
}
break;
} 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<SVGGElement>,
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 (
<g className="content">
<g className="arrows" fill={arrowColor} stroke={arrowColor}>
@ -395,7 +259,7 @@ export const GanttContent: React.FC<GanttContentProps> = ({
return task.barChildren.map(child => {
return (
<Arrow
key={`Arrow from ${task.id} to ${barTasks[child].id}`}
key={`Arrow from ${task.id} to ${tasks[child].id}`}
taskFrom={task}
taskTo={barTasks[child]}
rowHeight={rowHeight}
@ -414,19 +278,17 @@ export const GanttContent: React.FC<GanttContentProps> = ({
isProgressChangeable={!!onProgressChange && !task.isDisabled}
onDoubleClick={onDoubleClick}
isDateChangeable={!!onDateChange && !task.isDisabled}
handleMouseEvents={handleMouseEvents}
handleButtonSVGEvents={handleButtonSVGEvents}
isDelete={!!onTaskDelete && !task.isDisabled}
onEventStart={handleBarEventStart}
key={task.id}
/>
);
})}
</g>
<g className="toolTip">
{barEvent.selectedTask &&
barEvent.action !== "end" &&
barEvent.action !== "start" && (
{barEvent.selectedTask && (
<Tooltip
x={barEvent.selectedTask.x2 + columnWidth + arrowIndent}
x={barEvent.selectedTask.x2 + arrowIndent + arrowIndent * 0.5}
y={barEvent.selectedTask.y + rowHeight}
task={barEvent.selectedTask}
fontFamily={fontFamily}

View File

@ -33,7 +33,7 @@ export const Gantt: React.SFC<GanttProps> = ({
}) => {
const [startDate, endDate] = ganttDateRange(tasks, viewMode);
const dates = seedDates(startDate, endDate, viewMode);
const svg = useRef<SVGSVGElement | null>(null);
const svg = useRef<SVGSVGElement>(null);
const gridProps: GridProps = {
columnWidth,
@ -68,24 +68,23 @@ export const Gantt: React.SFC<GanttProps> = ({
handleWidth,
timeStep,
arrowColor,
svg,
fontFamily,
fontSize,
arrowIndent,
svg,
onDateChange,
onProgressChange,
onDoubleClick,
onTaskDelete,
getTooltipContent,
};
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={columnWidth * dates.length}
height={headerHeight + rowHeight * tasks.length}
ref={svg}
fontFamily={fontFamily}
ref={svg}
>
<Grid {...gridProps} />
<Calendar {...calendarProps} />

View File

@ -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 };
};

View File

@ -0,0 +1,5 @@
export function isKeyboardEvent(
event: React.MouseEvent | React.KeyboardEvent
): event is React.KeyboardEvent {
return (event as React.KeyboardEvent).key !== undefined;
}