init
This commit is contained in:
commit
063ced7795
42
.github/workflows/main.yml
vendored
Normal file
42
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: CI
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Begin CI...
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node 12
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
|
||||||
|
- name: Use cached node_modules
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: nodeModules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
nodeModules-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: yarn lint
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: yarn test --ci --coverage --maxWorkers=2
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"callout"
|
||||||
|
]
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 battakycyku@gmail.com
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
3
example/.npmignore
Normal file
3
example/.npmignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
15
example/index.html
Normal file
15
example/index.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
|
<title>Playground</title>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="./index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
259
example/index.tsx
Normal file
259
example/index.tsx
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import 'react-app-polyfill/ie11';
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
|
import {
|
||||||
|
Gantt,
|
||||||
|
Task,
|
||||||
|
EventOption,
|
||||||
|
StylingOption,
|
||||||
|
ViewMode,
|
||||||
|
DisplayOption,
|
||||||
|
} from '../src/index';
|
||||||
|
|
||||||
|
//Init
|
||||||
|
const App = () => {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const [view, setView] = React.useState<ViewMode>(ViewMode.Day);
|
||||||
|
let tasks: Task[] = [
|
||||||
|
{
|
||||||
|
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
|
||||||
|
end: new Date(
|
||||||
|
currentDate.getFullYear(),
|
||||||
|
currentDate.getMonth(),
|
||||||
|
2,
|
||||||
|
12,
|
||||||
|
28
|
||||||
|
),
|
||||||
|
name: 'Redesign website',
|
||||||
|
id: 'Task 0',
|
||||||
|
progress: 45,
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 2),
|
||||||
|
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4, 0, 0),
|
||||||
|
name: 'Write new content',
|
||||||
|
id: 'Task 1',
|
||||||
|
progress: 25,
|
||||||
|
dependencies: ['Task 0'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4),
|
||||||
|
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8, 0, 0),
|
||||||
|
name: 'Apply new styles',
|
||||||
|
id: 'Task 2',
|
||||||
|
progress: 10,
|
||||||
|
dependencies: ['Task 1'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
|
||||||
|
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 9, 0, 0),
|
||||||
|
name: 'Review',
|
||||||
|
id: 'Task currentDate.getMonth()',
|
||||||
|
progress: 2,
|
||||||
|
dependencies: ['Task 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
|
||||||
|
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 10),
|
||||||
|
name: 'Deploy',
|
||||||
|
id: 'Task 4',
|
||||||
|
progress: 70,
|
||||||
|
dependencies: ['Task 2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
|
||||||
|
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 26),
|
||||||
|
name: 'Go Live!',
|
||||||
|
id: 'Task 6',
|
||||||
|
progress: currentDate.getMonth(),
|
||||||
|
dependencies: ['Task 4'],
|
||||||
|
styles: { progressColor: '#ffbb54', progressSelectedColor: '#ff9e0d' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let onTaskChange = (task: Task) => {
|
||||||
|
console.log('On date change');
|
||||||
|
};
|
||||||
|
|
||||||
|
let onTaskDelete = (task: Task) => {
|
||||||
|
const conf = confirm('Are you sure?');
|
||||||
|
if (!conf) throw 'No del';
|
||||||
|
};
|
||||||
|
|
||||||
|
let onProgressChange = (task: Task) => {
|
||||||
|
console.log('On progress change');
|
||||||
|
};
|
||||||
|
|
||||||
|
let onDblClick = (task: Task) => {
|
||||||
|
alert('On Double Click event');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ViewSwitcher onViewChange={viewMode => setView(viewMode)} />
|
||||||
|
<GanttTableExample
|
||||||
|
tasks={tasks}
|
||||||
|
viewMode={view}
|
||||||
|
onDateChange={onTaskChange}
|
||||||
|
onTaskDelete={onTaskDelete}
|
||||||
|
onProgressChange={onProgressChange}
|
||||||
|
onDoubleClick={onDblClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//Gantt with Custom table example
|
||||||
|
type GanttTableExampleProps = { tasks: Task[] } & EventOption & DisplayOption;
|
||||||
|
export const GanttTableExample: React.SFC<GanttTableExampleProps> = props => {
|
||||||
|
const gridColumnWidth = 150;
|
||||||
|
let options: StylingOption = {
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily:
|
||||||
|
'Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue',
|
||||||
|
headerHeight: 50,
|
||||||
|
rowHeight: 50,
|
||||||
|
};
|
||||||
|
if (props.viewMode === ViewMode.Month) {
|
||||||
|
options.columnWidth = 300;
|
||||||
|
} else if (props.viewMode === ViewMode.Week) {
|
||||||
|
options.columnWidth = 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tasks, setTasks] = React.useState(props.tasks);
|
||||||
|
const onTaskDateChange = async (task: Task) => {
|
||||||
|
if (props.onDateChange) {
|
||||||
|
try {
|
||||||
|
await props.onDateChange(task);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
setTasks(tasks.map(t => (t.id === task.id ? task : t)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTaskProgressChange = async (task: Task) => {
|
||||||
|
if (props.onProgressChange) {
|
||||||
|
try {
|
||||||
|
await props.onProgressChange(task);
|
||||||
|
} catch (e) {
|
||||||
|
setTasks(props.tasks.slice());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTasks(tasks.map(t => (t.id === task.id ? task : t)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTaskItemDelete = async (task: Task) => {
|
||||||
|
if (props.onTaskDelete) {
|
||||||
|
await props.onTaskDelete(task);
|
||||||
|
setTasks(tasks.filter(t => t.id !== task.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Wrapper">
|
||||||
|
<div
|
||||||
|
className="GanttTable"
|
||||||
|
style={{
|
||||||
|
fontFamily: options.fontFamily,
|
||||||
|
fontSize: options.fontSize,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="GanttTable-header"
|
||||||
|
style={{
|
||||||
|
height: options.headerHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="GanttTable-headerItem"
|
||||||
|
style={{
|
||||||
|
minWidth: gridColumnWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span role="img" aria-label="fromDate" className="GanttTable-icon">
|
||||||
|
📃
|
||||||
|
</span>
|
||||||
|
Name
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="GanttTable-headerItem"
|
||||||
|
style={{
|
||||||
|
minWidth: gridColumnWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span role="img" aria-label="fromDate" className="GanttTable-icon">
|
||||||
|
📅
|
||||||
|
</span>
|
||||||
|
From
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="GanttTable-headerItem"
|
||||||
|
style={{
|
||||||
|
minWidth: gridColumnWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span role="img" aria-label="toDate" className="GanttTable-icon">
|
||||||
|
📅
|
||||||
|
</span>
|
||||||
|
To
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tasks.map(t => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="GanttTable-row"
|
||||||
|
style={{ height: options.rowHeight }}
|
||||||
|
>
|
||||||
|
<div className="GanttTable-cell">{t.name}</div>
|
||||||
|
<div className="GanttTable-cell">{t.start.toDateString()}</div>
|
||||||
|
<div className="GanttTable-cell">{t.end.toDateString()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div style={{ overflowX: 'scroll' }}>
|
||||||
|
<Gantt
|
||||||
|
{...options}
|
||||||
|
{...props}
|
||||||
|
tasks={tasks}
|
||||||
|
onDateChange={onTaskDateChange}
|
||||||
|
onTaskDelete={onTaskItemDelete}
|
||||||
|
onProgressChange={onTaskProgressChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ViewSwitcherProps = {
|
||||||
|
onViewChange: (viewMode: ViewMode) => void;
|
||||||
|
};
|
||||||
|
const ViewSwitcher: React.SFC<ViewSwitcherProps> = ({ onViewChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="ViewContainer">
|
||||||
|
<button
|
||||||
|
className="Button"
|
||||||
|
onClick={() => onViewChange(ViewMode.QuarterDay)}
|
||||||
|
>
|
||||||
|
Quarter of Day
|
||||||
|
</button>
|
||||||
|
<button className="Button" onClick={() => onViewChange(ViewMode.HalfDay)}>
|
||||||
|
Half of Day
|
||||||
|
</button>
|
||||||
|
<button className="Button" onClick={() => onViewChange(ViewMode.Day)}>
|
||||||
|
Day
|
||||||
|
</button>
|
||||||
|
<button className="Button" onClick={() => onViewChange(ViewMode.Week)}>
|
||||||
|
Week
|
||||||
|
</button>
|
||||||
|
<button className="Button" onClick={() => onViewChange(ViewMode.Month)}>
|
||||||
|
Month
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
7052
example/package-lock.json
generated
Normal file
7052
example/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
example/package.json
Normal file
24
example/package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"start": "parcel index.html",
|
||||||
|
"build": "parcel build index.html"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-app-polyfill": "^1.0.0"
|
||||||
|
},
|
||||||
|
"alias": {
|
||||||
|
"react": "../node_modules/react",
|
||||||
|
"react-dom": "../node_modules/react-dom/profiling",
|
||||||
|
"scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^16.9.11",
|
||||||
|
"@types/react-dom": "^16.8.4",
|
||||||
|
"parcel": "^1.12.3",
|
||||||
|
"typescript": "^3.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
67
example/style.css
Normal file
67
example/style.css
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/*Styles for Example Table*/
|
||||||
|
.Wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
background: #ffff;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttTable {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttTable-header {
|
||||||
|
display: table-row;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttTable-headerItem {
|
||||||
|
display: table-cell;
|
||||||
|
padding-top: 24px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
|
||||||
|
border-left: #e6e4e4 1px solid;
|
||||||
|
border-top: #e6e4e4 1px solid;
|
||||||
|
border-bottom: #e6e4e4 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttTable-row {
|
||||||
|
display: table-row;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttTable-row:nth-of-type(odd) {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttTable-cell {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-left: #e6e4e4 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttTable-icon {
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ViewContainer {
|
||||||
|
list-style: none;
|
||||||
|
-ms-box-orient: horizontal;
|
||||||
|
display: flex;
|
||||||
|
-webkit-justify-content: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button {
|
||||||
|
background-color: #e7e7e7;
|
||||||
|
color: black;
|
||||||
|
border: none;
|
||||||
|
padding: 7px 16px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 4px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
19
example/tsconfig.json
Normal file
19
example/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowSyntheticDefaultImports": false,
|
||||||
|
"target": "es5",
|
||||||
|
"module": "commonjs",
|
||||||
|
"jsx": "react",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"removeComments": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"lib": ["es2015", "es2016", "dom"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
5
jest.config.js
Normal file
5
jest.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^.+\\.(css|less|scss)$': 'babel-jest',
|
||||||
|
},
|
||||||
|
};
|
||||||
9676
package-lock.json
generated
Normal file
9676
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
package.json
Normal file
52
package.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsdx watch",
|
||||||
|
"build": "tsdx build",
|
||||||
|
"test": "tsdx test --passWithNoTests",
|
||||||
|
"lint": "tsdx lint",
|
||||||
|
"prepare": "tsdx build"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "tsdx lint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 80,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
},
|
||||||
|
"name": "gantt-task-react",
|
||||||
|
"author": "Maksym Vikarii maksym.vikarii@gmail.com",
|
||||||
|
"module": "dist/gantt-task-react.esm.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^16.9.41",
|
||||||
|
"@types/react-dom": "^16.9.8",
|
||||||
|
"husky": "^4.2.5",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"tsdx": "^0.13.2",
|
||||||
|
"tslib": "^2.0.0",
|
||||||
|
"typescript": "^3.9.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"autoprefixer": "^9.8.5",
|
||||||
|
"cssnano": "^4.1.10",
|
||||||
|
"rollup-plugin-postcss": "^3.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/components/Bar/bar-date-handle.tsx
Normal file
32
src/components/Bar/bar-date-handle.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../../style.css';
|
||||||
|
|
||||||
|
type BarDateHandleProps = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
barCornerRadius: number;
|
||||||
|
onMouseDown: (event: React.MouseEvent<SVGRectElement, MouseEvent>) => void;
|
||||||
|
};
|
||||||
|
export const BarDateHandle: React.FC<BarDateHandleProps> = ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
barCornerRadius,
|
||||||
|
onMouseDown,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className="GanttBar-handle"
|
||||||
|
ry={barCornerRadius}
|
||||||
|
rx={barCornerRadius}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
></rect>
|
||||||
|
);
|
||||||
|
};
|
||||||
98
src/components/Bar/bar-display.tsx
Normal file
98
src/components/Bar/bar-display.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
import '../../style.css';
|
||||||
|
|
||||||
|
type BarDisplayProps = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
progressWidth: number;
|
||||||
|
barCornerRadius: number;
|
||||||
|
text: string;
|
||||||
|
hasChild: boolean;
|
||||||
|
arrowIndent: number;
|
||||||
|
styles?: {
|
||||||
|
backgroundColor?: string;
|
||||||
|
backgroundSelectedColor?: string;
|
||||||
|
progressColor?: string;
|
||||||
|
progressSelectedColor?: string;
|
||||||
|
};
|
||||||
|
onMouseDown: (event: React.MouseEvent<SVGPolygonElement, MouseEvent>) => void;
|
||||||
|
};
|
||||||
|
export const BarDisplay: React.FC<BarDisplayProps> = ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
isSelected,
|
||||||
|
progressWidth,
|
||||||
|
barCornerRadius,
|
||||||
|
text,
|
||||||
|
hasChild,
|
||||||
|
arrowIndent,
|
||||||
|
styles,
|
||||||
|
onMouseDown,
|
||||||
|
}) => {
|
||||||
|
const textRef = useRef<SVGTextElement>(null);
|
||||||
|
const [isTextInside, setIsTextInside] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textRef.current)
|
||||||
|
setIsTextInside(textRef.current.getBBox().width < width);
|
||||||
|
}, [textRef, width]);
|
||||||
|
|
||||||
|
const getProcessColor = () => {
|
||||||
|
if (isSelected) {
|
||||||
|
return styles?.progressSelectedColor || '#8282f5';
|
||||||
|
} else {
|
||||||
|
return styles?.progressColor || '#a3a3ff';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBarColor = () => {
|
||||||
|
if (isSelected) {
|
||||||
|
return styles?.backgroundSelectedColor || '#aeb8c2';
|
||||||
|
} else {
|
||||||
|
return styles?.backgroundColor || '#b8c2cc';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g onMouseDown={onMouseDown}>
|
||||||
|
<rect
|
||||||
|
x={x}
|
||||||
|
width={width}
|
||||||
|
y={y}
|
||||||
|
height={height}
|
||||||
|
ry={barCornerRadius}
|
||||||
|
rx={barCornerRadius}
|
||||||
|
fill={getBarColor()}
|
||||||
|
className="GanttBar"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x={x}
|
||||||
|
width={progressWidth}
|
||||||
|
y={y}
|
||||||
|
height={height}
|
||||||
|
ry={barCornerRadius}
|
||||||
|
rx={barCornerRadius}
|
||||||
|
fill={getProcessColor()}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={
|
||||||
|
isTextInside
|
||||||
|
? x + width * 0.5
|
||||||
|
: x + width + arrowIndent * +hasChild + arrowIndent * 0.2
|
||||||
|
}
|
||||||
|
y={y + height * 0.5}
|
||||||
|
className={`GanttBar-label ${
|
||||||
|
isTextInside ? '' : 'GanttBar-label-outside'
|
||||||
|
}`}
|
||||||
|
ref={textRef}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
19
src/components/Bar/bar-progress-handle.tsx
Normal file
19
src/components/Bar/bar-progress-handle.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../../style.css';
|
||||||
|
|
||||||
|
type BarProgressHandleProps = {
|
||||||
|
progressPoint: string;
|
||||||
|
onMouseDown: (event: React.MouseEvent<SVGPolygonElement, MouseEvent>) => void;
|
||||||
|
};
|
||||||
|
export const BarProgressHandle: React.FC<BarProgressHandleProps> = ({
|
||||||
|
progressPoint,
|
||||||
|
onMouseDown,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<polygon
|
||||||
|
className="GanttBar-handle"
|
||||||
|
points={progressPoint}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
></polygon>
|
||||||
|
);
|
||||||
|
};
|
||||||
126
src/components/Bar/bar.tsx
Normal file
126
src/components/Bar/bar.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Task } from '../../types/public-types';
|
||||||
|
import { BarProgressHandle } from './bar-progress-handle';
|
||||||
|
import { BarDateHandle } from './bar-date-handle';
|
||||||
|
import { BarDisplay } from './bar-display';
|
||||||
|
import { BarTask } from '../../types/bar-task';
|
||||||
|
import { BarEvent } from '../Gantt/gantt-content';
|
||||||
|
import {
|
||||||
|
progressWithByParams,
|
||||||
|
getProgressPoint,
|
||||||
|
} from '../../helpers/bar-helper';
|
||||||
|
import '../../style.css';
|
||||||
|
|
||||||
|
export type BarProps = {
|
||||||
|
task: BarTask;
|
||||||
|
arrowIndent: number;
|
||||||
|
onDoubleClick?: (task: Task) => any;
|
||||||
|
isProgressChangeable: boolean;
|
||||||
|
isDateChangeable: boolean;
|
||||||
|
handleMouseEvents: (
|
||||||
|
event:
|
||||||
|
| React.MouseEvent<SVGPolygonElement, MouseEvent>
|
||||||
|
| React.MouseEvent<SVGGElement, MouseEvent>
|
||||||
|
| React.MouseEvent<SVGRectElement, MouseEvent>,
|
||||||
|
eventType: BarEvent,
|
||||||
|
task: BarTask
|
||||||
|
) => void;
|
||||||
|
handleButtonSVGEvents: (
|
||||||
|
event: React.KeyboardEvent<SVGGElement>,
|
||||||
|
task: BarTask
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Bar: React.FC<BarProps> = ({
|
||||||
|
task,
|
||||||
|
arrowIndent,
|
||||||
|
onDoubleClick,
|
||||||
|
isProgressChangeable,
|
||||||
|
isDateChangeable,
|
||||||
|
handleMouseEvents,
|
||||||
|
handleButtonSVGEvents,
|
||||||
|
}) => {
|
||||||
|
const [isSelected, setIsSelected] = useState(false);
|
||||||
|
|
||||||
|
const progressWidth = progressWithByParams(task.x1, task.x2, task.progress);
|
||||||
|
const progressPoint = getProgressPoint(
|
||||||
|
progressWidth + task.x1,
|
||||||
|
task.y,
|
||||||
|
task.height
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
className="GanttBar-wrapper"
|
||||||
|
onDoubleClick={() => {
|
||||||
|
!!onDoubleClick && onDoubleClick(task);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => {
|
||||||
|
handleButtonSVGEvents(e, task);
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
handleMouseEvents(e, 'mouseenter', task);
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
handleMouseEvents(e, 'mouseleave', task);
|
||||||
|
}}
|
||||||
|
onFocus={() => setIsSelected(true)}
|
||||||
|
onBlur={() => setIsSelected(false)}
|
||||||
|
>
|
||||||
|
<BarDisplay
|
||||||
|
x={task.x1}
|
||||||
|
y={task.y}
|
||||||
|
width={task.x2 - task.x1}
|
||||||
|
height={task.height}
|
||||||
|
progressWidth={progressWidth}
|
||||||
|
barCornerRadius={task.barCornerRadius}
|
||||||
|
text={task.name}
|
||||||
|
hasChild={task.barChildren.length > 0}
|
||||||
|
arrowIndent={arrowIndent}
|
||||||
|
styles={task.styles}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onMouseDown={e => {
|
||||||
|
isDateChangeable && handleMouseEvents(e, 'move', task);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<g className="handleGroup">
|
||||||
|
{isDateChangeable && (
|
||||||
|
<>
|
||||||
|
{/*left*/}
|
||||||
|
<BarDateHandle
|
||||||
|
x={task.x1 + 1}
|
||||||
|
y={task.y + 1}
|
||||||
|
width={task.handleWidth}
|
||||||
|
height={task.height - 2}
|
||||||
|
barCornerRadius={task.barCornerRadius}
|
||||||
|
onMouseDown={e => {
|
||||||
|
handleMouseEvents(e, 'start', task);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/*right*/}
|
||||||
|
<BarDateHandle
|
||||||
|
x={task.x2 - task.handleWidth - 1}
|
||||||
|
y={task.y + 1}
|
||||||
|
width={task.handleWidth}
|
||||||
|
height={task.height - 2}
|
||||||
|
barCornerRadius={task.barCornerRadius}
|
||||||
|
onMouseDown={e => {
|
||||||
|
handleMouseEvents(e, 'end', task);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isProgressChangeable && (
|
||||||
|
<BarProgressHandle
|
||||||
|
progressPoint={progressPoint}
|
||||||
|
onMouseDown={e => {
|
||||||
|
console.log('progress');
|
||||||
|
handleMouseEvents(e, 'progress', task);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
215
src/components/Calendar/calendar.tsx
Normal file
215
src/components/Calendar/calendar.tsx
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import React, { ReactChild } from 'react';
|
||||||
|
import { ViewMode } from '../../types/public-types';
|
||||||
|
import { TopPartOfCalendar } from './top-part-of-calendar';
|
||||||
|
import {
|
||||||
|
getLocaleMonth,
|
||||||
|
getWeekNumberISO8601,
|
||||||
|
} from '../../helpers/date-helper';
|
||||||
|
import '../../style.css';
|
||||||
|
export type CalendarProps = {
|
||||||
|
dates: Date[];
|
||||||
|
locale: string;
|
||||||
|
viewMode: ViewMode;
|
||||||
|
headerHeight: number;
|
||||||
|
columnWidth: number;
|
||||||
|
fontFamily: string;
|
||||||
|
fontSize: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Calendar: React.FC<CalendarProps> = ({
|
||||||
|
dates,
|
||||||
|
locale,
|
||||||
|
viewMode,
|
||||||
|
headerHeight,
|
||||||
|
columnWidth,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
}) => {
|
||||||
|
const getCalendarValuesForMonth = () => {
|
||||||
|
let topValues: ReactChild[] = [];
|
||||||
|
let bottomValues: ReactChild[] = [];
|
||||||
|
const topDefaultWidth = columnWidth * 6;
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = dates[i];
|
||||||
|
let bottomValue = getLocaleMonth(date, locale);
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={bottomValue + date.getFullYear()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * i + columnWidth * 0.5}
|
||||||
|
className="GanttCalendar-bottomText"
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
if (i === 0 || date.getFullYear() !== dates[i - 1].getFullYear()) {
|
||||||
|
let topValue = date.getFullYear().toString();
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={
|
||||||
|
topDefaultWidth + columnWidth * i - date.getMonth() * columnWidth
|
||||||
|
}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForWeek = () => {
|
||||||
|
let topValues: ReactChild[] = [];
|
||||||
|
let bottomValues: ReactChild[] = [];
|
||||||
|
let weeksCount: number = 0;
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
for (let i = dates.length - 1; i >= 0; i--) {
|
||||||
|
const date = dates[i];
|
||||||
|
let topValue = '';
|
||||||
|
if (i === 0 || date.getMonth() !== dates[i - 1].getMonth()) {
|
||||||
|
//top
|
||||||
|
topValue = `${getLocaleMonth(date, locale)}, ${date.getFullYear()}`;
|
||||||
|
}
|
||||||
|
//bottom
|
||||||
|
const bottomValue = `W${getWeekNumberISO8601(date)}`;
|
||||||
|
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * i}
|
||||||
|
className="GanttCalendar-bottomText"
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (topValue) {
|
||||||
|
//if last day is new month
|
||||||
|
if (i !== dates.length - 1) {
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i + weeksCount * columnWidth}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={columnWidth * i + columnWidth * weeksCount * 0.5}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
weeksCount = 0;
|
||||||
|
}
|
||||||
|
weeksCount++;
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForDay = () => {
|
||||||
|
let topValues: ReactChild[] = [];
|
||||||
|
let bottomValues: ReactChild[] = [];
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = dates[i];
|
||||||
|
const bottomValue = date.getDate().toString();
|
||||||
|
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * i + columnWidth * 0.5}
|
||||||
|
className="GanttCalendar-bottomText"
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
i + 1 !== dates.length &&
|
||||||
|
date.getMonth() !== dates[i + 1].getMonth()
|
||||||
|
) {
|
||||||
|
let topValue = getLocaleMonth(date, locale);
|
||||||
|
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * (i + 1)}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={columnWidth * (i + 1) - date.getDate() * columnWidth * 0.5}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForOther = () => {
|
||||||
|
let topValues: ReactChild[] = [];
|
||||||
|
let bottomValues: ReactChild[] = [];
|
||||||
|
let ticks = viewMode === ViewMode.HalfDay ? 2 : 4;
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = dates[i];
|
||||||
|
const bottomValue = Intl.DateTimeFormat(locale, {
|
||||||
|
hour: 'numeric',
|
||||||
|
}).format(date);
|
||||||
|
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * i}
|
||||||
|
className="GanttCalendar-bottomText"
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
if (i === 0 || date.getDate() !== dates[i - 1].getDate()) {
|
||||||
|
const topValue = `${date.getDate()} ${getLocaleMonth(date, locale)}`;
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i + ticks * columnWidth}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={columnWidth * i + ticks * columnWidth * 0.5}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
let topValues: ReactChild[] = [];
|
||||||
|
let bottomValues: ReactChild[] = [];
|
||||||
|
switch (viewMode) {
|
||||||
|
case ViewMode.Month:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForMonth();
|
||||||
|
break;
|
||||||
|
case ViewMode.Week:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForWeek();
|
||||||
|
break;
|
||||||
|
case ViewMode.Day:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForDay();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForOther();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<g className="calendar" fontSize={fontSize} fontFamily={fontFamily}>
|
||||||
|
{bottomValues} {topValues}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
41
src/components/Calendar/top-part-of-calendar.tsx
Normal file
41
src/components/Calendar/top-part-of-calendar.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../../style.css';
|
||||||
|
|
||||||
|
type TopPartOfCalendarProps = {
|
||||||
|
value: string;
|
||||||
|
x1Line: number;
|
||||||
|
y1Line: number;
|
||||||
|
y2Line: number;
|
||||||
|
xText: number;
|
||||||
|
yText: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TopPartOfCalendar: React.FC<TopPartOfCalendarProps> = ({
|
||||||
|
value,
|
||||||
|
x1Line,
|
||||||
|
y1Line,
|
||||||
|
y2Line,
|
||||||
|
xText,
|
||||||
|
yText,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<line
|
||||||
|
x1={x1Line}
|
||||||
|
y1={y1Line}
|
||||||
|
x2={x1Line}
|
||||||
|
y2={y2Line}
|
||||||
|
className="GanttCalendar-topTick"
|
||||||
|
key={value + 'line'}
|
||||||
|
></line>
|
||||||
|
<text
|
||||||
|
key={value + 'text'}
|
||||||
|
y={yText}
|
||||||
|
x={xText}
|
||||||
|
className="GanttCalendar-topText"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
403
src/components/Gantt/gantt-content.tsx
Normal file
403
src/components/Gantt/gantt-content.tsx
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
import React, { useState, useEffect } 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,
|
||||||
|
} from '../../helpers/bar-helper';
|
||||||
|
import { Tooltip } from '../Other/tooltip';
|
||||||
|
export interface GanttTask extends Task {
|
||||||
|
x1: number;
|
||||||
|
x2: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
export type GanttContentProps = {
|
||||||
|
tasks: Task[];
|
||||||
|
dates: Date[];
|
||||||
|
rowHeight: number;
|
||||||
|
barCornerRadius: number;
|
||||||
|
columnWidth: number;
|
||||||
|
barFill: number;
|
||||||
|
headerHeight: number;
|
||||||
|
handleWidth: number;
|
||||||
|
svg: React.MutableRefObject<SVGSVGElement | null>;
|
||||||
|
timeStep: number;
|
||||||
|
arrowColor: string;
|
||||||
|
arrowIndent: number;
|
||||||
|
fontSize: string;
|
||||||
|
fontFamily: string;
|
||||||
|
getTooltipContent?: (
|
||||||
|
task: Task,
|
||||||
|
fontSize: string,
|
||||||
|
fontFamily: string
|
||||||
|
) => JSX.Element;
|
||||||
|
} & EventOption;
|
||||||
|
|
||||||
|
export type BarEvent =
|
||||||
|
| 'progress'
|
||||||
|
| 'end'
|
||||||
|
| 'start'
|
||||||
|
| 'move'
|
||||||
|
| 'mouseenter'
|
||||||
|
| 'mouseleave'
|
||||||
|
| '';
|
||||||
|
export const GanttContent: React.FC<GanttContentProps> = ({
|
||||||
|
tasks,
|
||||||
|
rowHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
columnWidth,
|
||||||
|
dates,
|
||||||
|
barFill,
|
||||||
|
headerHeight,
|
||||||
|
handleWidth,
|
||||||
|
arrowColor,
|
||||||
|
svg,
|
||||||
|
timeStep,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
arrowIndent,
|
||||||
|
onDateChange,
|
||||||
|
onProgressChange,
|
||||||
|
onDoubleClick,
|
||||||
|
onTaskDelete,
|
||||||
|
getTooltipContent,
|
||||||
|
}) => {
|
||||||
|
const [barEvent, setBarEvent] = useState<BarEvent>('');
|
||||||
|
const [selectedTask, setSelectedTask] = useState<BarTask | null>(null);
|
||||||
|
const [barTasks, setBarTasks] = useState<BarTask[]>([]);
|
||||||
|
const [xStep, setXStep] = useState(0);
|
||||||
|
const [initEventX1Delta, setInitEventX1Delta] = useState(0);
|
||||||
|
const [isSVGListen, setIsSVGListen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dateDelta =
|
||||||
|
dates[1].getTime() -
|
||||||
|
dates[0].getTime() -
|
||||||
|
dates[1].getTimezoneOffset() * 60 * 1000 +
|
||||||
|
dates[0].getTimezoneOffset() * 60 * 1000;
|
||||||
|
const newXStep = (timeStep * columnWidth) / dateDelta;
|
||||||
|
if (newXStep !== xStep) {
|
||||||
|
setXStep(newXStep);
|
||||||
|
}
|
||||||
|
}, [tasks, rowHeight, barCornerRadius, columnWidth, dates, timeStep, xStep]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dateDelta =
|
||||||
|
dates[1].getTime() -
|
||||||
|
dates[0].getTime() -
|
||||||
|
dates[1].getTimezoneOffset() * 60 * 1000 +
|
||||||
|
dates[0].getTimezoneOffset() * 60 * 1000;
|
||||||
|
const taskHeight = (rowHeight * barFill) / 100;
|
||||||
|
|
||||||
|
setBarTasks(
|
||||||
|
convertToBarTasks(
|
||||||
|
tasks,
|
||||||
|
dates,
|
||||||
|
dateDelta,
|
||||||
|
columnWidth,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
headerHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
tasks,
|
||||||
|
rowHeight,
|
||||||
|
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 (!selectedTask || !barEvent) return;
|
||||||
|
const changedTask = { ...selectedTask } as BarTask;
|
||||||
|
switch (event.type) {
|
||||||
|
//On Event changing
|
||||||
|
case 'mousemove': {
|
||||||
|
switch (barEvent) {
|
||||||
|
case 'progress':
|
||||||
|
changedTask.progress = progressByX(event.offsetX, selectedTask);
|
||||||
|
break;
|
||||||
|
case 'start':
|
||||||
|
let newX1 = startByX(event.offsetX, xStep, selectedTask);
|
||||||
|
changedTask.x1 = newX1;
|
||||||
|
changedTask.start = dateByX(
|
||||||
|
newX1,
|
||||||
|
selectedTask.x1,
|
||||||
|
selectedTask.start,
|
||||||
|
xStep,
|
||||||
|
timeStep
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'end':
|
||||||
|
let 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))
|
||||||
|
);
|
||||||
|
setSelectedTask(changedTask);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//On finish Event
|
||||||
|
case 'mouseup': {
|
||||||
|
let eventForExecution: (
|
||||||
|
task: Task
|
||||||
|
) => void | Promise<void> = () => {};
|
||||||
|
switch (barEvent) {
|
||||||
|
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('');
|
||||||
|
setSelectedTask(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 (selectedTask && barEvent && !isSVGListen) {
|
||||||
|
svg.current?.addEventListener(
|
||||||
|
'mousemove',
|
||||||
|
handleMouseSVGChangeEventsSubscribe
|
||||||
|
);
|
||||||
|
svg.current?.addEventListener(
|
||||||
|
'mouseup',
|
||||||
|
handleMouseSVGChangeEventsSubscribe
|
||||||
|
);
|
||||||
|
setIsSVGListen(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
barEvent,
|
||||||
|
selectedTask,
|
||||||
|
xStep,
|
||||||
|
svg,
|
||||||
|
initEventX1Delta,
|
||||||
|
barTasks,
|
||||||
|
onProgressChange,
|
||||||
|
timeStep,
|
||||||
|
onDateChange,
|
||||||
|
isSVGListen,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: BarEvent,
|
||||||
|
task: BarTask
|
||||||
|
) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'mousedown':
|
||||||
|
setBarEvent(eventType);
|
||||||
|
setSelectedTask(task);
|
||||||
|
setInitEventX1Delta(event.nativeEvent.offsetX - task.x1);
|
||||||
|
event.stopPropagation();
|
||||||
|
break;
|
||||||
|
case 'mouseleave':
|
||||||
|
if (!barEvent) setSelectedTask(null);
|
||||||
|
break;
|
||||||
|
case 'mouseenter':
|
||||||
|
if (!selectedTask) {
|
||||||
|
setSelectedTask(task);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<g className="arrow" fill={arrowColor} stroke={arrowColor}>
|
||||||
|
{barTasks.map(task => {
|
||||||
|
return task.barChildren.map(child => {
|
||||||
|
return (
|
||||||
|
<Arrow
|
||||||
|
key={`Arrow from ${task.id} to ${barTasks[child].id}`}
|
||||||
|
taskFrom={task}
|
||||||
|
taskTo={barTasks[child]}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
arrowIndent={arrowIndent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
<g className="bar" fontFamily={fontFamily} fontSize={fontSize}>
|
||||||
|
{barTasks.map(task => {
|
||||||
|
return (
|
||||||
|
<Bar
|
||||||
|
task={task}
|
||||||
|
arrowIndent={arrowIndent}
|
||||||
|
isProgressChangeable={!!onProgressChange && !task.isDisabled}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
isDateChangeable={!!onDateChange && !task.isDisabled}
|
||||||
|
handleMouseEvents={handleMouseEvents}
|
||||||
|
handleButtonSVGEvents={handleButtonSVGEvents}
|
||||||
|
key={task.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
<g className="toolTip">
|
||||||
|
{selectedTask && barEvent !== 'end' && barEvent !== 'start' && (
|
||||||
|
<Tooltip
|
||||||
|
x={selectedTask.x2 + columnWidth + arrowIndent}
|
||||||
|
y={selectedTask.y + rowHeight}
|
||||||
|
task={selectedTask}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
fontSize={fontSize}
|
||||||
|
getTooltipContent={getTooltipContent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
85
src/components/Gantt/gantt.tsx
Normal file
85
src/components/Gantt/gantt.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { ViewMode, GanttProps } from '../../types/public-types';
|
||||||
|
import { Grid, GridProps } from '../Grid/grid';
|
||||||
|
import { Calendar, CalendarProps } from '../Calendar/calendar';
|
||||||
|
import { GanttContent, GanttContentProps } from './gantt-content';
|
||||||
|
import { ganttDateRange, seedDates } from '../../helpers/date-helper';
|
||||||
|
|
||||||
|
export const Gantt: React.SFC<GanttProps> = ({
|
||||||
|
tasks,
|
||||||
|
headerHeight = 50,
|
||||||
|
columnWidth = 60,
|
||||||
|
rowHeight = 50,
|
||||||
|
viewMode = ViewMode.Day,
|
||||||
|
locale = 'en-GB',
|
||||||
|
barFill = 60,
|
||||||
|
barCornerRadius = 3,
|
||||||
|
handleWidth = 8,
|
||||||
|
timeStep = 300000,
|
||||||
|
arrowColor = 'grey',
|
||||||
|
fontFamily = 'Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue',
|
||||||
|
fontSize = '14px',
|
||||||
|
arrowIndent = 20,
|
||||||
|
onDateChange,
|
||||||
|
onProgressChange,
|
||||||
|
onDoubleClick,
|
||||||
|
onTaskDelete,
|
||||||
|
getTooltipContent,
|
||||||
|
}) => {
|
||||||
|
const [startDate, endDate] = ganttDateRange(tasks, viewMode);
|
||||||
|
const dates = seedDates(startDate, endDate, viewMode);
|
||||||
|
const svg = useRef<SVGSVGElement | null>(null);
|
||||||
|
|
||||||
|
const gridProps: GridProps = {
|
||||||
|
columnWidth,
|
||||||
|
gridWidth: dates.length * columnWidth,
|
||||||
|
tasks,
|
||||||
|
rowHeight,
|
||||||
|
headerHeight,
|
||||||
|
dates,
|
||||||
|
};
|
||||||
|
const calendarProps: CalendarProps = {
|
||||||
|
dates,
|
||||||
|
locale,
|
||||||
|
viewMode,
|
||||||
|
headerHeight,
|
||||||
|
columnWidth,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
};
|
||||||
|
const barProps: GanttContentProps = {
|
||||||
|
tasks,
|
||||||
|
rowHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
columnWidth,
|
||||||
|
dates,
|
||||||
|
barFill,
|
||||||
|
headerHeight,
|
||||||
|
handleWidth,
|
||||||
|
timeStep,
|
||||||
|
arrowColor,
|
||||||
|
svg,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
arrowIndent,
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<Grid {...gridProps} />
|
||||||
|
<Calendar {...calendarProps} />
|
||||||
|
<GanttContent {...barProps} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
99
src/components/Grid/grid-body.tsx
Normal file
99
src/components/Grid/grid-body.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, { ReactChild } from 'react';
|
||||||
|
import { Task } from '../../types/public-types';
|
||||||
|
import { addToDate } from '../../helpers/date-helper';
|
||||||
|
import '../../style.css';
|
||||||
|
|
||||||
|
export type GridBodyProps = {
|
||||||
|
tasks: Task[];
|
||||||
|
dates: Date[];
|
||||||
|
gridWidth: number;
|
||||||
|
rowHeight: number;
|
||||||
|
headerHeight: number;
|
||||||
|
columnWidth: number;
|
||||||
|
};
|
||||||
|
export const GridBody: React.FC<GridBodyProps> = ({
|
||||||
|
tasks,
|
||||||
|
dates,
|
||||||
|
rowHeight,
|
||||||
|
headerHeight,
|
||||||
|
gridWidth,
|
||||||
|
columnWidth,
|
||||||
|
}) => {
|
||||||
|
let y = headerHeight;
|
||||||
|
let gridRows: ReactChild[] = [];
|
||||||
|
let rowLines: ReactChild[] = [];
|
||||||
|
for (const task of tasks) {
|
||||||
|
gridRows.push(
|
||||||
|
<rect
|
||||||
|
key={'Row' + task.id}
|
||||||
|
x="0"
|
||||||
|
y={y}
|
||||||
|
width={gridWidth}
|
||||||
|
height={rowHeight}
|
||||||
|
className="GanttGrid-row"
|
||||||
|
></rect>
|
||||||
|
);
|
||||||
|
rowLines.push(
|
||||||
|
<line
|
||||||
|
key={'RowLine' + task.id}
|
||||||
|
x="0"
|
||||||
|
y1={y + rowHeight}
|
||||||
|
x2={gridWidth}
|
||||||
|
y2={y + rowHeight}
|
||||||
|
className="GanttGrid-rowLine"
|
||||||
|
></line>
|
||||||
|
);
|
||||||
|
y += rowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
let tickX = 0;
|
||||||
|
let ticks: ReactChild[] = [];
|
||||||
|
let today: ReactChild = <></>;
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = dates[i];
|
||||||
|
ticks.push(
|
||||||
|
<line
|
||||||
|
key={date.getTime()}
|
||||||
|
x1={tickX}
|
||||||
|
y1={headerHeight}
|
||||||
|
x2={tickX}
|
||||||
|
y2={y}
|
||||||
|
className="GanttGrid-tick"
|
||||||
|
></line>
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
(i + 1 !== dates.length &&
|
||||||
|
date.getTime() < now.getTime() &&
|
||||||
|
dates[i + 1].getTime() >= now.getTime()) ||
|
||||||
|
//if current date is last
|
||||||
|
(i !== 0 &&
|
||||||
|
i + 1 === dates.length &&
|
||||||
|
date.getTime() < now.getTime() &&
|
||||||
|
addToDate(
|
||||||
|
date,
|
||||||
|
date.getTime() - dates[i - 1].getTime(),
|
||||||
|
'millisecond'
|
||||||
|
).getTime() >= now.getTime())
|
||||||
|
) {
|
||||||
|
today = (
|
||||||
|
<rect
|
||||||
|
x={tickX}
|
||||||
|
y={0}
|
||||||
|
width={columnWidth}
|
||||||
|
height={y}
|
||||||
|
className="GanttGrid-todayHighlight"
|
||||||
|
></rect>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tickX += columnWidth;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<g className="rows">{gridRows}</g>
|
||||||
|
<g className="rowLines">{rowLines}</g>
|
||||||
|
<g className="ticks">{ticks}</g>
|
||||||
|
<g className="today">{today}</g>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
21
src/components/Grid/grid-header.tsx
Normal file
21
src/components/Grid/grid-header.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../../style.css';
|
||||||
|
|
||||||
|
export type GridHeaderProps = {
|
||||||
|
gridWidth: number;
|
||||||
|
headerHeight: number;
|
||||||
|
};
|
||||||
|
export const GridHeader: React.FC<GridHeaderProps> = ({
|
||||||
|
gridWidth,
|
||||||
|
headerHeight,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width={gridWidth}
|
||||||
|
height={headerHeight}
|
||||||
|
className="GanttGrid-header"
|
||||||
|
></rect>
|
||||||
|
);
|
||||||
|
};
|
||||||
13
src/components/Grid/grid.tsx
Normal file
13
src/components/Grid/grid.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { GridBody, GridBodyProps } from "./grid-body";
|
||||||
|
import { GridHeader, GridHeaderProps } from "./grid-header";
|
||||||
|
|
||||||
|
export type GridProps = GridBodyProps & GridHeaderProps;
|
||||||
|
export const Grid: React.FC<GridProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<g className="grid">
|
||||||
|
<GridHeader {...props} />
|
||||||
|
<GridBody {...props} />
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
src/components/Other/arrow.tsx
Normal file
34
src/components/Other/arrow.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { BarTask } from "../../types/bar-task";
|
||||||
|
|
||||||
|
type ArrowProps = {
|
||||||
|
taskFrom: BarTask;
|
||||||
|
taskTo: BarTask;
|
||||||
|
rowHeight: number;
|
||||||
|
arrowIndent: number;
|
||||||
|
};
|
||||||
|
export const Arrow: React.FC<ArrowProps> = ({
|
||||||
|
taskFrom,
|
||||||
|
taskTo,
|
||||||
|
rowHeight,
|
||||||
|
arrowIndent,
|
||||||
|
}) => {
|
||||||
|
const indexCompare = taskFrom.index > taskTo.index ? -1 : 1;
|
||||||
|
const taskToEndPosition = taskTo.y + taskTo.height / 2;
|
||||||
|
|
||||||
|
const path = `M ${taskFrom.x2} ${taskFrom.y + taskFrom.height / 2}
|
||||||
|
h ${arrowIndent}
|
||||||
|
v ${(indexCompare * rowHeight) / 2}
|
||||||
|
H ${taskTo.x1 - arrowIndent}
|
||||||
|
V ${taskToEndPosition}
|
||||||
|
h ${arrowIndent}`;
|
||||||
|
const trianglePoints = `${taskTo.x1},${taskToEndPosition}
|
||||||
|
${taskTo.x1 - 5},${taskToEndPosition - 5}
|
||||||
|
${taskTo.x1 - 5},${taskToEndPosition + 5}`;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<path strokeWidth="1.5" d={path} fill="none" />
|
||||||
|
<polygon points={trianglePoints} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
69
src/components/Other/tooltip.tsx
Normal file
69
src/components/Other/tooltip.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import { Task } from '../../types/public-types';
|
||||||
|
import '../../style.css';
|
||||||
|
export type TooltipProps = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
task: Task;
|
||||||
|
fontSize: string;
|
||||||
|
fontFamily: string;
|
||||||
|
getTooltipContent?: (
|
||||||
|
task: Task,
|
||||||
|
fontSize: string,
|
||||||
|
fontFamily: string
|
||||||
|
) => JSX.Element;
|
||||||
|
};
|
||||||
|
export const Tooltip: React.FC<TooltipProps> = ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
task,
|
||||||
|
fontSize,
|
||||||
|
fontFamily,
|
||||||
|
|
||||||
|
getTooltipContent = getStandardTooltipContent,
|
||||||
|
}) => {
|
||||||
|
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [toolWidth, setToolWidth] = useState(1000);
|
||||||
|
const [relatedY, setRelatedY] = useState(y);
|
||||||
|
useEffect(() => {
|
||||||
|
if (tooltipRef.current) {
|
||||||
|
const height =
|
||||||
|
tooltipRef.current.offsetHeight +
|
||||||
|
tooltipRef.current.offsetHeight * 0.15;
|
||||||
|
setRelatedY(y - height);
|
||||||
|
setToolWidth(tooltipRef.current.scrollWidth * 1.1);
|
||||||
|
}
|
||||||
|
}, [tooltipRef, y]);
|
||||||
|
return (
|
||||||
|
<foreignObject x={x} y={relatedY} width={toolWidth} height={1000}>
|
||||||
|
<div ref={tooltipRef} className="TooltipDetailsContainer">
|
||||||
|
{getTooltipContent(task, fontSize, fontFamily)}
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStandardTooltipContent = (
|
||||||
|
task: Task,
|
||||||
|
fontSize: string,
|
||||||
|
fontFamily: string
|
||||||
|
) => {
|
||||||
|
const style = {
|
||||||
|
fontSize,
|
||||||
|
fontFamily,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="TooltipDefaultContainer" style={style}>
|
||||||
|
<b style={{ fontSize: fontSize + 6 }}>{`${
|
||||||
|
task.name
|
||||||
|
}: ${task.start.getDate()}-${task.start.getMonth() +
|
||||||
|
1}-${task.start.getFullYear()} - ${task.end.getDate()}-${task.end.getMonth() +
|
||||||
|
1}-${task.end.getFullYear()}`}</b>
|
||||||
|
<p className="TooltipDefaultContainer-paragraph">{`Duration: ${~~(
|
||||||
|
(task.end.getTime() - task.start.getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24)
|
||||||
|
)} day(s)`}</p>
|
||||||
|
<p className="TooltipDefaultContainer-paragraph">{`Progress: ${task.progress} %`}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
195
src/helpers/bar-helper.ts
Normal file
195
src/helpers/bar-helper.ts
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import { Task } from "../types/public-types";
|
||||||
|
import { BarTask } from "../types/bar-task";
|
||||||
|
|
||||||
|
export const convertToBarTasks = (
|
||||||
|
tasks: Task[],
|
||||||
|
dates: Date[],
|
||||||
|
dateDelta: number,
|
||||||
|
columnWidth: number,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
headerHeight: number,
|
||||||
|
barCornerRadius: number,
|
||||||
|
handleWidth: number
|
||||||
|
) => {
|
||||||
|
let barTasks = tasks.map((t, i) => {
|
||||||
|
return convertToBarTask(
|
||||||
|
t,
|
||||||
|
i,
|
||||||
|
dates,
|
||||||
|
dateDelta,
|
||||||
|
columnWidth,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
headerHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
barTasks = barTasks.map((task, i) => {
|
||||||
|
const dependencies = task.dependencies || [];
|
||||||
|
for (let j = 0; j < dependencies.length; j++) {
|
||||||
|
const dependence = barTasks.findIndex(
|
||||||
|
(value) => value.id === dependencies[j]
|
||||||
|
);
|
||||||
|
if (dependence !== -1) barTasks[dependence].barChildren.push(i);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
});
|
||||||
|
|
||||||
|
return barTasks;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToBarTask = (
|
||||||
|
task: Task,
|
||||||
|
index: number,
|
||||||
|
dates: Date[],
|
||||||
|
dateDelta: number,
|
||||||
|
columnWidth: number,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
headerHeight: number,
|
||||||
|
barCornerRadius: number,
|
||||||
|
handleWidth: number
|
||||||
|
): BarTask => {
|
||||||
|
const x1 = taskXCoordinate(task.start, dates, dateDelta, columnWidth);
|
||||||
|
const x2 = taskXCoordinate(task.end, dates, dateDelta, columnWidth);
|
||||||
|
const y = taskYCoordinate(index, rowHeight, taskHeight, headerHeight);
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
x1,
|
||||||
|
x2,
|
||||||
|
y,
|
||||||
|
index,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth,
|
||||||
|
height: taskHeight,
|
||||||
|
barChildren: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const taskXCoordinate = (
|
||||||
|
xDate: Date,
|
||||||
|
dates: Date[],
|
||||||
|
dateDelta: number,
|
||||||
|
columnWidth: number
|
||||||
|
) => {
|
||||||
|
const index = ~~(
|
||||||
|
(xDate.getTime() -
|
||||||
|
dates[0].getTime() +
|
||||||
|
xDate.getTimezoneOffset() -
|
||||||
|
dates[0].getTimezoneOffset()) /
|
||||||
|
dateDelta
|
||||||
|
);
|
||||||
|
const x = Math.round(
|
||||||
|
(index +
|
||||||
|
(xDate.getTime() -
|
||||||
|
dates[index].getTime() -
|
||||||
|
xDate.getTimezoneOffset() * 60 * 1000 +
|
||||||
|
dates[index].getTimezoneOffset() * 60 * 1000) /
|
||||||
|
dateDelta) *
|
||||||
|
columnWidth
|
||||||
|
);
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const taskYCoordinate = (
|
||||||
|
index: number,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
headerHeight: number
|
||||||
|
) => {
|
||||||
|
const y = index * rowHeight + headerHeight + (rowHeight - taskHeight) / 2;
|
||||||
|
return y;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const progressWithByParams = (
|
||||||
|
taskX1: number,
|
||||||
|
taskX2: number,
|
||||||
|
progress: number
|
||||||
|
) => {
|
||||||
|
return (taskX2 - taskX1) * progress * 0.01;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const progressByProgressWidth = (
|
||||||
|
progressWidth: number,
|
||||||
|
barTask: BarTask
|
||||||
|
) => {
|
||||||
|
const barWidth = barTask.x2 - barTask.x1;
|
||||||
|
const progressPercent = Math.round((progressWidth * 100) / barWidth);
|
||||||
|
if (progressPercent >= 100) return 100;
|
||||||
|
else if (progressPercent <= 0) return 0;
|
||||||
|
else {
|
||||||
|
return progressPercent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const progressByX = (x: number, task: BarTask) => {
|
||||||
|
if (x >= task.x2) return 100;
|
||||||
|
else if (x <= task.x1) return 0;
|
||||||
|
else {
|
||||||
|
const barWidth = task.x2 - task.x1;
|
||||||
|
const progressPercent = Math.round(((x - task.x1) * 100) / barWidth);
|
||||||
|
return progressPercent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProgressPoint = (
|
||||||
|
progressX: number,
|
||||||
|
taskY: number,
|
||||||
|
taskHeight: number
|
||||||
|
) => {
|
||||||
|
const point = [
|
||||||
|
progressX - 5,
|
||||||
|
taskY + taskHeight,
|
||||||
|
progressX + 5,
|
||||||
|
taskY + taskHeight,
|
||||||
|
progressX,
|
||||||
|
taskY + taskHeight - 8.66,
|
||||||
|
];
|
||||||
|
return point.join(",");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startByX = (x: number, xStep: number, task: BarTask) => {
|
||||||
|
if (x >= task.x2 - task.handleWidth * 2) {
|
||||||
|
x = task.x2 - task.handleWidth * 2;
|
||||||
|
}
|
||||||
|
const steps = Math.round((x - task.x1) / xStep);
|
||||||
|
const additionalXValue = steps * xStep;
|
||||||
|
const newX = task.x1 + additionalXValue;
|
||||||
|
return newX;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const endByX = (x: number, xStep: number, task: BarTask) => {
|
||||||
|
if (x <= task.x1 + task.handleWidth * 2) {
|
||||||
|
x = task.x1 + task.handleWidth * 2;
|
||||||
|
}
|
||||||
|
const steps = Math.round((x - task.x2) / xStep);
|
||||||
|
const additionalXValue = steps * xStep;
|
||||||
|
const newX = task.x2 + additionalXValue;
|
||||||
|
return newX;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveByX = (x: number, xStep: number, task: BarTask) => {
|
||||||
|
const steps = Math.round((x - task.x1) / xStep);
|
||||||
|
const additionalXValue = steps * xStep;
|
||||||
|
const newX1 = task.x1 + additionalXValue;
|
||||||
|
const newX2 = newX1 + task.x2 - task.x1;
|
||||||
|
return [newX1, newX2];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dateByX = (
|
||||||
|
x: number,
|
||||||
|
taskX: number,
|
||||||
|
taskDate: Date,
|
||||||
|
xStep: number,
|
||||||
|
timeStep: number
|
||||||
|
) => {
|
||||||
|
let newDate = new Date(((x - taskX) / xStep) * timeStep + taskDate.getTime());
|
||||||
|
newDate = new Date(
|
||||||
|
newDate.getTime() +
|
||||||
|
(newDate.getTimezoneOffset() - taskDate.getTimezoneOffset()) * 60000
|
||||||
|
);
|
||||||
|
return newDate;
|
||||||
|
};
|
||||||
150
src/helpers/date-helper.ts
Normal file
150
src/helpers/date-helper.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { Task, ViewMode } from '../types/public-types';
|
||||||
|
|
||||||
|
type DateHelperScales =
|
||||||
|
| 'year'
|
||||||
|
| 'month'
|
||||||
|
| 'day'
|
||||||
|
| 'hour'
|
||||||
|
| 'minute'
|
||||||
|
| 'second'
|
||||||
|
| 'millisecond';
|
||||||
|
|
||||||
|
export const addToDate = (
|
||||||
|
date: Date,
|
||||||
|
quantity: number,
|
||||||
|
scale: DateHelperScales
|
||||||
|
) => {
|
||||||
|
let newDate = new Date(
|
||||||
|
date.getFullYear() + (scale === 'year' ? quantity : 0),
|
||||||
|
date.getMonth() + (scale === 'month' ? quantity : 0),
|
||||||
|
date.getDate() + (scale === 'day' ? quantity : 0),
|
||||||
|
date.getHours() + (scale === 'hour' ? quantity : 0),
|
||||||
|
date.getMinutes() + (scale === 'minute' ? quantity : 0),
|
||||||
|
date.getSeconds() + (scale === 'second' ? quantity : 0),
|
||||||
|
date.getMilliseconds() + (scale === 'millisecond' ? quantity : 0)
|
||||||
|
);
|
||||||
|
return newDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startOfDate = (date: Date, scale: DateHelperScales) => {
|
||||||
|
const scores = [
|
||||||
|
'millisecond',
|
||||||
|
'second',
|
||||||
|
'minute',
|
||||||
|
'hour',
|
||||||
|
'day',
|
||||||
|
'month',
|
||||||
|
'year',
|
||||||
|
];
|
||||||
|
|
||||||
|
const shouldReset = (_scale: DateHelperScales) => {
|
||||||
|
const max_score = scores.indexOf(scale);
|
||||||
|
return scores.indexOf(_scale) <= max_score;
|
||||||
|
};
|
||||||
|
let newDate = new Date(
|
||||||
|
date.getFullYear(),
|
||||||
|
shouldReset('year') ? 0 : date.getMonth(),
|
||||||
|
shouldReset('month') ? 1 : date.getDate(),
|
||||||
|
shouldReset('day') ? 0 : date.getHours(),
|
||||||
|
shouldReset('hour') ? 0 : date.getMinutes(),
|
||||||
|
shouldReset('minute') ? 0 : date.getSeconds(),
|
||||||
|
shouldReset('second') ? 0 : date.getMilliseconds()
|
||||||
|
);
|
||||||
|
return newDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ganttDateRange = (tasks: Task[], viewMode: ViewMode) => {
|
||||||
|
let newStartDate: Date = tasks[0].start;
|
||||||
|
let newEndDate: Date = tasks[0].end;
|
||||||
|
for (let task of tasks) {
|
||||||
|
if (task.start < newStartDate) {
|
||||||
|
newStartDate = task.start;
|
||||||
|
}
|
||||||
|
if (task.end > newEndDate) {
|
||||||
|
newEndDate = task.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewMode === ViewMode.Month) {
|
||||||
|
newStartDate = addToDate(newStartDate, -1, 'month');
|
||||||
|
newEndDate = addToDate(newEndDate, 1, 'year');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'year');
|
||||||
|
} else if (viewMode === ViewMode.Week) {
|
||||||
|
newStartDate = startOfDate(newStartDate, 'day');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'day');
|
||||||
|
|
||||||
|
newStartDate = addToDate(getMonday(newStartDate), -7, 'day');
|
||||||
|
newEndDate = addToDate(newEndDate, 1.5, 'month');
|
||||||
|
} else {
|
||||||
|
newStartDate = startOfDate(newStartDate, 'day');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'day');
|
||||||
|
newStartDate = addToDate(newStartDate, -1, 'day');
|
||||||
|
newEndDate = addToDate(newEndDate, 19, 'day');
|
||||||
|
}
|
||||||
|
return [newStartDate, newEndDate];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const seedDates = (
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date,
|
||||||
|
viewMode: ViewMode
|
||||||
|
) => {
|
||||||
|
let currentDate: Date = new Date(startDate);
|
||||||
|
let dates: Date[] = [currentDate];
|
||||||
|
while (currentDate < endDate) {
|
||||||
|
if (viewMode === ViewMode.Month) {
|
||||||
|
currentDate = addToDate(currentDate, 1, 'month');
|
||||||
|
} else if (viewMode === ViewMode.Week) {
|
||||||
|
currentDate = addToDate(currentDate, 7, 'day');
|
||||||
|
} else if (viewMode === ViewMode.Day) {
|
||||||
|
currentDate = addToDate(currentDate, 1, 'day');
|
||||||
|
} else if (viewMode === ViewMode.HalfDay) {
|
||||||
|
currentDate = addToDate(currentDate, 12, 'hour');
|
||||||
|
} else if (viewMode === ViewMode.QuarterDay) {
|
||||||
|
currentDate = addToDate(currentDate, 6, 'hour');
|
||||||
|
}
|
||||||
|
dates.push(currentDate);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLocaleMonth = (date: Date, locale: string) => {
|
||||||
|
let bottomValue = new Intl.DateTimeFormat(locale, {
|
||||||
|
month: 'long',
|
||||||
|
}).format(date);
|
||||||
|
bottomValue = bottomValue.replace(
|
||||||
|
bottomValue[0],
|
||||||
|
bottomValue[0].toLocaleUpperCase()
|
||||||
|
);
|
||||||
|
return bottomValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns monday of current week
|
||||||
|
* @param date date for modify
|
||||||
|
*/
|
||||||
|
const getMonday = (date: Date) => {
|
||||||
|
const day = date.getDay();
|
||||||
|
const diff = date.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday
|
||||||
|
return new Date(date.setDate(diff));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWeekNumberISO8601 = (date: Date) => {
|
||||||
|
let tmpDate = new Date(date.valueOf());
|
||||||
|
const dayNumber = (tmpDate.getDay() + 6) % 7;
|
||||||
|
tmpDate.setDate(tmpDate.getDate() - dayNumber + 3);
|
||||||
|
const firstThursday = tmpDate.valueOf();
|
||||||
|
tmpDate.setMonth(0, 1);
|
||||||
|
if (tmpDate.getDay() !== 4) {
|
||||||
|
tmpDate.setMonth(0, 1 + ((4 - tmpDate.getDay() + 7) % 7));
|
||||||
|
}
|
||||||
|
const weekNumber = (
|
||||||
|
1 + Math.ceil((firstThursday - tmpDate.valueOf()) / 604800000)
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
if (weekNumber.length === 1) {
|
||||||
|
return `0${weekNumber}`;
|
||||||
|
} else {
|
||||||
|
return weekNumber;
|
||||||
|
}
|
||||||
|
};
|
||||||
9
src/index.tsx
Normal file
9
src/index.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export { Gantt } from './components/Gantt/gantt';
|
||||||
|
export {
|
||||||
|
GanttProps,
|
||||||
|
Task,
|
||||||
|
ViewMode,
|
||||||
|
StylingOption,
|
||||||
|
DisplayOption,
|
||||||
|
EventOption,
|
||||||
|
} from './types/public-types';
|
||||||
34
src/reducers/reducers.ts
Normal file
34
src/reducers/reducers.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Task } from "../types/public-types";
|
||||||
|
|
||||||
|
type GanttReduceState = {
|
||||||
|
ganttTasks: Task[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GanttReduceAction = {
|
||||||
|
type: "update" | "delete";
|
||||||
|
changedTask?: Task;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ganttReducer(
|
||||||
|
state: GanttReduceState,
|
||||||
|
action: GanttReduceAction
|
||||||
|
): GanttReduceState {
|
||||||
|
switch (action.type) {
|
||||||
|
case "update": {
|
||||||
|
return {
|
||||||
|
ganttTasks: state.ganttTasks.map((t) =>
|
||||||
|
t.id === action.changedTask?.id ? action.changedTask : t
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "delete": {
|
||||||
|
return {
|
||||||
|
ganttTasks: state.ganttTasks.filter(
|
||||||
|
(t) => t.id !== action.changedTask?.id
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/style.css
Normal file
109
src/style.css
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
.GanttBar {
|
||||||
|
user-select: none;
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttBar-wrapper {
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttBar-wrapper:hover .GanttBar-handle {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttBar-label {
|
||||||
|
fill: #fff;
|
||||||
|
text-anchor: middle;
|
||||||
|
font-weight: lighter;
|
||||||
|
dominant-baseline: central;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttBar-label-outside {
|
||||||
|
fill: #555;
|
||||||
|
text-anchor: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttBar-handle {
|
||||||
|
fill: #ddd;
|
||||||
|
cursor: ew-resize;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttCalendar-bottomText {
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #333;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttCalendar-topTick {
|
||||||
|
stroke: #e6e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttCalendar-topText {
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #555;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttGrid-row {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttGrid-row:nth-child(even) {
|
||||||
|
fill: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttGrid-header {
|
||||||
|
fill: #ffffff;
|
||||||
|
stroke: #e0e0e0;
|
||||||
|
stroke-width: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttGrid-rowLine {
|
||||||
|
stroke: #ebeff2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttGrid-tick {
|
||||||
|
stroke: #e6e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.GanttGrid-todayHighlight {
|
||||||
|
fill: #fcf8e3;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipDefaultContainer {
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipDefaultContainer-paragraph {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipDetailsContainer {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
12
src/types/bar-task.ts
Normal file
12
src/types/bar-task.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Task } from "./public-types";
|
||||||
|
|
||||||
|
export interface BarTask extends Task {
|
||||||
|
index: number;
|
||||||
|
x1: number;
|
||||||
|
x2: number;
|
||||||
|
y: number;
|
||||||
|
height: number;
|
||||||
|
barCornerRadius: number;
|
||||||
|
handleWidth: number;
|
||||||
|
barChildren: number[];
|
||||||
|
}
|
||||||
71
src/types/public-types.ts
Normal file
71
src/types/public-types.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
export enum ViewMode {
|
||||||
|
QuarterDay = 'Quarter Day',
|
||||||
|
HalfDay = 'Half Day',
|
||||||
|
Day = 'Day',
|
||||||
|
/** ISO-8601 week */
|
||||||
|
Week = 'Week',
|
||||||
|
Month = 'Month',
|
||||||
|
}
|
||||||
|
export interface Task {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
/**
|
||||||
|
* From 0 to 100
|
||||||
|
*/
|
||||||
|
progress: number;
|
||||||
|
styles?: {
|
||||||
|
backgroundColor?: string;
|
||||||
|
backgroundSelectedColor?: string;
|
||||||
|
progressColor?: string;
|
||||||
|
progressSelectedColor?: string;
|
||||||
|
};
|
||||||
|
isDisabled?: boolean;
|
||||||
|
dependencies?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventOption {
|
||||||
|
/**
|
||||||
|
* Time step value for date changes.
|
||||||
|
*/
|
||||||
|
timeStep?: number;
|
||||||
|
onDoubleClick?: (task: Task) => any;
|
||||||
|
onDateChange?: (task: Task) => void | Promise<any>;
|
||||||
|
onProgressChange?: (task: Task) => void | Promise<any>;
|
||||||
|
onTaskDelete?: (task: Task) => void | Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisplayOption {
|
||||||
|
viewMode?: ViewMode;
|
||||||
|
/**
|
||||||
|
* Display date format. Able formats: ISO 639-2, Java Locale
|
||||||
|
*/
|
||||||
|
locale?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StylingOption {
|
||||||
|
headerHeight?: number;
|
||||||
|
columnWidth?: number;
|
||||||
|
rowHeight?: number;
|
||||||
|
barCornerRadius?: number;
|
||||||
|
handleWidth?: number;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
/**
|
||||||
|
* How many of row width can be taken by task.
|
||||||
|
* From 0 to 100
|
||||||
|
*/
|
||||||
|
barFill?: number;
|
||||||
|
arrowColor?: string;
|
||||||
|
arrowIndent?: number;
|
||||||
|
getTooltipContent?: (
|
||||||
|
task: Task,
|
||||||
|
fontSize: string,
|
||||||
|
fontFamily: string
|
||||||
|
) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GanttProps extends EventOption, DisplayOption, StylingOption {
|
||||||
|
tasks: Task[];
|
||||||
|
}
|
||||||
73
test/date-helper.test.tsx
Normal file
73
test/date-helper.test.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
seedDates,
|
||||||
|
addToDate,
|
||||||
|
getWeekNumberISO8601,
|
||||||
|
} from '../src/helpers/date-helper';
|
||||||
|
import { ViewMode } from '../src/types/public-types';
|
||||||
|
|
||||||
|
describe('seed date', () => {
|
||||||
|
test('daily', () => {
|
||||||
|
expect(
|
||||||
|
seedDates(new Date(2020, 5, 28), new Date(2020, 6, 2), ViewMode.Day)
|
||||||
|
).toEqual([
|
||||||
|
new Date(2020, 5, 28),
|
||||||
|
new Date(2020, 5, 29),
|
||||||
|
new Date(2020, 5, 30),
|
||||||
|
new Date(2020, 6, 1),
|
||||||
|
new Date(2020, 6, 2),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('weekly', () => {
|
||||||
|
expect(
|
||||||
|
seedDates(new Date(2020, 5, 28), new Date(2020, 6, 19), ViewMode.Week)
|
||||||
|
).toEqual([
|
||||||
|
new Date(2020, 5, 28),
|
||||||
|
new Date(2020, 6, 5),
|
||||||
|
new Date(2020, 6, 12),
|
||||||
|
new Date(2020, 6, 19),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('monthly', () => {
|
||||||
|
expect(
|
||||||
|
seedDates(new Date(2020, 5, 28), new Date(2020, 6, 19), ViewMode.Month)
|
||||||
|
).toEqual([new Date(2020, 5, 28), new Date(2020, 6, 28)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('quarterly', () => {
|
||||||
|
expect(
|
||||||
|
seedDates(
|
||||||
|
new Date(2020, 5, 28),
|
||||||
|
new Date(2020, 5, 29),
|
||||||
|
ViewMode.QuarterDay
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
new Date(2020, 5, 28, 0, 0),
|
||||||
|
new Date(2020, 5, 28, 6, 0),
|
||||||
|
new Date(2020, 5, 28, 12, 0),
|
||||||
|
new Date(2020, 5, 28, 18, 0),
|
||||||
|
new Date(2020, 5, 29, 0, 0),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('add to date', () => {
|
||||||
|
test('add month', () => {
|
||||||
|
expect(addToDate(new Date(2020, 0, 1), 40, 'month')).toEqual(
|
||||||
|
new Date(2023, 4, 1)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add day', () => {
|
||||||
|
expect(addToDate(new Date(2020, 0, 1), 40, 'day')).toEqual(
|
||||||
|
new Date(2020, 1, 10)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get week number', () => {
|
||||||
|
expect(getWeekNumberISO8601(new Date(2019, 11, 31))).toEqual('01');
|
||||||
|
expect(getWeekNumberISO8601(new Date(2021, 0, 1))).toEqual('53');
|
||||||
|
expect(getWeekNumberISO8601(new Date(2020, 6, 20))).toEqual('30');
|
||||||
|
});
|
||||||
24
test/gant.test.tsx
Normal file
24
test/gant.test.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Gantt } from '../src/index';
|
||||||
|
|
||||||
|
describe('gantt', () => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
ReactDOM.render(
|
||||||
|
<Gantt
|
||||||
|
tasks={[
|
||||||
|
{
|
||||||
|
start: new Date(2020, 0, 1),
|
||||||
|
end: new Date(2020, 2, 2),
|
||||||
|
name: 'Redesign website',
|
||||||
|
id: 'Task 0',
|
||||||
|
progress: 45,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
div
|
||||||
|
);
|
||||||
|
ReactDOM.unmountComponentAtNode(div);
|
||||||
|
});
|
||||||
|
});
|
||||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"include": ["src", "types", "tsdx.config.js", "tsdx.config.js"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"lib": ["dom", "esnext"],
|
||||||
|
"importHelpers": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"*": ["src/*", "node_modules/*"]
|
||||||
|
},
|
||||||
|
"jsx": "react",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
20
tsdx.config.js
Normal file
20
tsdx.config.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const postcss = require('rollup-plugin-postcss');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
const cssnano = require('cssnano');
|
||||||
|
module.exports = {
|
||||||
|
rollup(config, options) {
|
||||||
|
config.plugins.push(
|
||||||
|
postcss({
|
||||||
|
plugins: [
|
||||||
|
autoprefixer(),
|
||||||
|
cssnano({
|
||||||
|
preset: 'default',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
inject: false,
|
||||||
|
extract: !!options.writeMeta,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user