Skip to content

Commit

Permalink
Merge pull request #44 from melfore/develop
Browse files Browse the repository at this point in the history
Task drag & drop support
  • Loading branch information
luciob committed Sep 6, 2023
2 parents 9cc1f95 + 9599335 commit 43f8313
Show file tree
Hide file tree
Showing 15 changed files with 300 additions and 133 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# [1.2.0](https://github.com/melfore/konva-timeline/compare/v1.1.0...v1.2.0) (2023-08-31)


### Features

* offset applied also in x dimension ([17c71ec](https://github.com/melfore/konva-timeline/commit/17c71ec3235c4e04439098688543be6053dbd847))
- offset applied also in x dimension ([17c71ec](https://github.com/melfore/konva-timeline/commit/17c71ec3235c4e04439098688543be6053dbd847))

# [1.1.0](https://github.com/melfore/konva-timeline/compare/v1.0.3...v1.1.0) (2023-08-31)

Expand Down
31 changes: 25 additions & 6 deletions src/@components/GridLayer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ interface GridLayerProps {
}

const GridLayer: FC<GridLayerProps> = ({ columnWidth, height, width }) => {
const { drawRange, interval, resolution, resources, timeBlocks } = useTimelineContext();
const {
drawRange,
interval,
resolution,
resources,
theme: { color: themeColor },
timeBlocks,
} = useTimelineContext();

const { sizeInUnits, unit, unitAbove } = resolution;

Expand Down Expand Up @@ -40,8 +47,15 @@ const GridLayer: FC<GridLayerProps> = ({ columnWidth, height, width }) => {

return (
<KonvaGroup>
<KonvaRect fill="white" x={5 + unitAboveStartX} y={5} height={18} width={oneUnitAboveColumnWidth - 10} />
<KonvaRect
fill="transparent"
x={5 + unitAboveStartX}
y={5}
height={18}
width={oneUnitAboveColumnWidth - 10}
/>
<KonvaText
fill={themeColor}
x={5 + unitAboveStartX}
y={10}
text={displayInterval(unitAboveInterval, unitAbove)}
Expand All @@ -52,7 +66,7 @@ const GridLayer: FC<GridLayerProps> = ({ columnWidth, height, width }) => {
</KonvaGroup>
);
},
[height, oneUnitAboveDuration, oneUnitAboveColumnWidth, unitAbove, unitAboveIntervals]
[height, oneUnitAboveDuration, oneUnitAboveColumnWidth, themeColor, unitAbove, unitAboveIntervals]
);

return (
Expand All @@ -61,7 +75,7 @@ const GridLayer: FC<GridLayerProps> = ({ columnWidth, height, width }) => {
{resources.map(({ id }, index) => {
return (
<KonvaGroup key={`heading-${id}`}>
<KonvaLine x={0} y={50 * (index + 1)} points={[0, 0, width, 0]} stroke="black" />
<KonvaLine x={0} y={50 * (index + 1)} points={[0, 0, width, 0]} stroke={themeColor} />
</KonvaGroup>
);
})}
Expand All @@ -76,8 +90,13 @@ const GridLayer: FC<GridLayerProps> = ({ columnWidth, height, width }) => {
<KonvaGroup key={`timeslot-${index}`}>
{gridLabels(index)}
<KonvaLine x={columnWidth * index} y={40} points={[0, 0, 0, height]} stroke="gray" strokeWidth={1} />
<KonvaRect fill="white" x={columnWidth * index - 15} y={30} height={15} width={30} />
<KonvaText x={columnWidth * index - 15} y={32} text={displayInterval(column, resolution.unit)} />
<KonvaRect fill="transparent" x={columnWidth * index - 15} y={30} height={15} width={30} />
<KonvaText
fill={themeColor}
x={columnWidth * index - 15}
y={32}
text={displayInterval(column, resolution.unit)}
/>
</KonvaGroup>
);
})}
Expand Down
9 changes: 7 additions & 2 deletions src/@components/ResourceHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { memo, useMemo } from "react";

import { useTimelineContext } from "../../@contexts/Timeline";
import {
Resource,
RESOURCE_HEADER_HEIGHT,
Expand All @@ -18,16 +19,20 @@ interface ResourceHeaderProps extends Resource {
* The playground has a simulated canvas with height: 200px and width: 100%
*/
const ResourceHeader = ({ index, label }: ResourceHeaderProps) => {
const {
theme: { color: themeColor },
} = useTimelineContext();

const yCoordinate = useMemo(() => RESOURCE_HEADER_HEIGHT * (index + 1), [index]);

const textYCoordinate = useMemo(() => RESOURCE_HEADER_HEIGHT * index, [index]);

return (
<KonvaGroup x={0} y={0}>
<KonvaGroup x={RESOURCE_HEADER_TEXT_OFFSET} y={RESOURCE_HEADER_TEXT_OFFSET}>
<KonvaText text={label} y={textYCoordinate} />
<KonvaText fill={themeColor} text={label} y={textYCoordinate} />
</KonvaGroup>
<KonvaLine points={[0, 0, RESOURCE_HEADER_WIDTH, 0]} stroke="black" y={yCoordinate} />
<KonvaLine points={[0, 0, RESOURCE_HEADER_WIDTH, 0]} stroke={themeColor} y={yCoordinate} />
</KonvaGroup>
);
};
Expand Down
119 changes: 104 additions & 15 deletions src/@components/Task/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import React, { memo, useCallback } from "react";
import React, { memo, useCallback, useMemo, useState } from "react";
import { Rect } from "react-konva";
import { KonvaEventObject } from "konva/lib/Node";

import { useTimelineContext } from "../../@contexts/Timeline";
import { KonvaDrawable, KonvaPoint } from "../../@utils/konva";
import { RESOURCE_HEADER_HEIGHT, RESOURCE_HEADER_OFFSET } from "../../@utils/resources";
import { TaskData } from "../../@utils/tasks";

type TaskMouseEventHandler = (taskId: string, point: KonvaPoint) => void;

type TaskProps = Pick<TaskData, "id" | "label"> &
KonvaDrawable &
type TaskProps = KonvaDrawable &
KonvaPoint & {
/**
* On mouse click event handler
* Task data (id, label, resourceId, time)
*/
onClick: TaskMouseEventHandler;
data: TaskData;
/**
* On mouse leave event handler
*/
Expand All @@ -28,7 +29,7 @@ type TaskProps = Pick<TaskData, "id" | "label"> &
width: number;
};

const TASK_DEFAULT_FILL = "white";
const TASK_DEFAULT_FILL = "transparent";
const TASK_DEFAULT_STROKE = "black";

const TASK_BORDER_RADIUS = 4;
Expand All @@ -46,16 +47,60 @@ const TASK_HEIGHT = 40;
* The playground has a simulated canvas with height: 200px and width: 100%
*/
const Task = ({
data,
fill = TASK_DEFAULT_FILL,
id,
onClick,
onLeave,
onOver,
stroke = TASK_DEFAULT_STROKE,
x,
y,
width,
}: TaskProps) => {
const {
columnWidth,
interval,
onTaskClick,
onTaskDrag,
resolution: { sizeInUnits, unit },
resources,
} = useTimelineContext();

const { id: taskId } = data;

const [dragging, setDragging] = useState(false);

const getBoundedCoordinates = useCallback((xCoordinate: number, resourceIndex: number): KonvaPoint => {
const boundedX = xCoordinate < 0 ? 0 : xCoordinate;
const boundedY = resourceIndex * RESOURCE_HEADER_HEIGHT + RESOURCE_HEADER_OFFSET;

return { x: boundedX, y: boundedY };
}, []);

const getDragPoint = useCallback((e: KonvaEventObject<DragEvent>): KonvaPoint => {
const { target } = e;
const dragX = target.x();
const dragY = target.y();

return { x: dragX, y: dragY };
}, []);

const getResourceIndexFromYCoordinate = useCallback(
(yCoordinate: number) => {
const rawIndex = Math.floor(yCoordinate / RESOURCE_HEADER_HEIGHT);
if (rawIndex < 1) {
return 1;
}

const lastResourceIndex = resources.length - 1;
if (rawIndex > lastResourceIndex) {
return lastResourceIndex;
}

return rawIndex;
},
[resources]
);

const onTaskMouseEvent = useCallback(
(e: KonvaEventObject<MouseEvent>, callback: TaskMouseEventHandler) => {
const stage = e.target.getStage();
Expand All @@ -68,14 +113,14 @@ const Task = ({
return;
}

callback(id, point);
callback(taskId, point);
},
[id]
[taskId]
);

const onTaskClick = useCallback(
(e: KonvaEventObject<MouseEvent>) => onTaskMouseEvent(e, onClick),
[onClick, onTaskMouseEvent]
const onClick = useCallback(
(e: KonvaEventObject<MouseEvent>) => onTaskClick && onTaskClick(data),
[data, onTaskClick]
);

const onTaskLeave = useCallback(
Expand All @@ -88,16 +133,60 @@ const Task = ({
[onOver, onTaskMouseEvent]
);

const onDragStart = useCallback((e: KonvaEventObject<DragEvent>) => setDragging(true), []);

const onDragMove = useCallback(
(e: KonvaEventObject<DragEvent>) => {
const { x, y } = getDragPoint(e);
const resourceIndex = getResourceIndexFromYCoordinate(y);
const point = getBoundedCoordinates(x, resourceIndex);
e.target.setPosition(point);
onOver(taskId, point);
},
[getBoundedCoordinates, getDragPoint, getResourceIndexFromYCoordinate, onOver, taskId]
);

const onDragEnd = useCallback(
(e: KonvaEventObject<DragEvent>) => {
const { x, y } = getDragPoint(e);
const timeOffset = (x * sizeInUnits) / columnWidth;
const newMillis = interval.start!.plus({ [unit]: timeOffset }).toMillis();
const resourceIndex = getResourceIndexFromYCoordinate(y);
const resourceId = `${resourceIndex}`;
console.log(`New Start: ${x} / ${x} / ${timeOffset} / ${newMillis}`);
setDragging(false);
onTaskDrag && onTaskDrag({ ...data, resourceId, time: { end: newMillis + width, start: newMillis } });
},
[
columnWidth,
data,
interval.start,
onTaskDrag,
getDragPoint,
getResourceIndexFromYCoordinate,
sizeInUnits,
unit,
width,
]
);

const opacity = useMemo(() => (dragging ? 0.5 : 1), [dragging]);

return (
<Rect
id={id}
id={taskId}
cornerRadius={TASK_BORDER_RADIUS}
draggable={!!onTaskDrag}
fill={fill}
height={TASK_HEIGHT}
onClick={onTaskClick}
onClick={onClick}
onDragStart={onDragStart}
onDragMove={onDragMove}
onDragEnd={onDragEnd}
onMouseLeave={onTaskLeave}
onMouseMove={onTaskOver}
onMouseOver={onTaskOver}
opacity={opacity}
stroke={stroke}
x={x}
y={y}
Expand Down
21 changes: 0 additions & 21 deletions src/@components/TaskTooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,4 @@ const TaskTooltip: FC<TaskTooltipProps> = ({ task: { label: taskLabel }, x, y })
);
};

// return taskTooltipContent ? (
// <Html
// transform={false}
// divProps={{
// style: {
// border: "1px solid black",
// backgroundColor: "white",
// padding: "16px",
// position: "fixed",
// top: 200,
// left: 200,
// zIndex: 100,
// },
// }}
// >
// {taskTooltipContent(taskTooltip.task)}
// </Html>
// ) : (
// <TaskTooltip {...taskTooltip} />
// );

export default TaskTooltip;
25 changes: 6 additions & 19 deletions src/@components/TasksLayer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const TASK_PLACEMENT_OFFSET = 5;
*/
const TasksLayer: FC<TasksLayerProps> = () => {
const {
columnWidth,
drawRange,
interval: { start: intervalStart, end: intervalEnd },
resolution,
Expand All @@ -39,19 +40,6 @@ const TasksLayer: FC<TasksLayerProps> = () => {

const getTaskById = useCallback((taskId: string) => tasks.find(({ id }) => taskId === id), [tasks]);

const onTaskClick = useCallback(
(taskId: string, point: KonvaPoint) => {
const task = getTaskById(taskId);
if (!task) {
return;
}

// TODO#lb: add real implementation
alert(`You clicked on task '${task.label}'. Point x: ${point.x}, y: ${point.y}`);
},
[getTaskById]
);

const onTaskLeave = useCallback(() => setTaskTooltip(null), []);

const onTaskOver = useCallback(
Expand All @@ -68,8 +56,8 @@ const TasksLayer: FC<TasksLayerProps> = () => {
);

const getXCoordinate = useCallback(
(offset: number) => (offset * resolution.columnSize) / resolution.sizeInUnits,
[resolution.columnSize, resolution.sizeInUnits]
(offset: number) => (offset * columnWidth) / resolution.sizeInUnits,
[columnWidth, resolution.sizeInUnits]
);

const getTaskXCoordinate = useCallback(
Expand Down Expand Up @@ -97,7 +85,8 @@ const TasksLayer: FC<TasksLayerProps> = () => {

return (
<Layer>
{tasks.map(({ id, label, resourceId, time }, index) => {
{tasks.map((taskData, index) => {
const { resourceId, time } = taskData;
const resourceIndex = getResourceById(resourceId);
if (resourceIndex < 0) {
return null;
Expand All @@ -114,10 +103,8 @@ const TasksLayer: FC<TasksLayerProps> = () => {
return (
<Task
key={`task-${index}`}
id={id}
data={taskData}
fill={resourceColor}
label={label}
onClick={onTaskClick}
onLeave={onTaskLeave}
onOver={onTaskOver}
x={xCoordinate}
Expand Down
Loading

0 comments on commit 43f8313

Please sign in to comment.