├── .DS_Store
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── examples
├── from-readme.html
├── index.html
├── vue.edit.html
├── vue.html
└── vue.max-rows.html
├── gantt-elastic.gif
├── gantt-elastic.jpg
├── grab-scroll.gif
├── jest.config.js
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── GanttElastic.tsx
├── GanttElasticContext.ts
├── GanttElasticEvents.ts
├── components
│ ├── Chart
│ │ ├── Calendar
│ │ │ ├── Calendar.tsx
│ │ │ ├── CalendarRow.tsx
│ │ │ └── RowText.tsx
│ │ ├── Chart.tsx
│ │ ├── DaysHighlight.tsx
│ │ ├── DependencyLines.tsx
│ │ ├── Grid.tsx
│ │ ├── ProgressBar.tsx
│ │ ├── Row
│ │ │ ├── Milestone.tsx
│ │ │ ├── Project.tsx
│ │ │ └── Task.tsx
│ │ └── Text.tsx
│ ├── Expander.tsx
│ ├── MainView.tsx
│ ├── TaskList
│ │ ├── ItemColumn.tsx
│ │ ├── TaskList.tsx
│ │ ├── TaskListHeader.tsx
│ │ └── TaskListItem.tsx
│ ├── interfaces.d.ts
│ └── utils
│ │ ├── charts.ts
│ │ ├── columns.ts
│ │ ├── options.ts
│ │ ├── style.ts
│ │ ├── tasks.ts
│ │ └── times.ts
├── demo.tsx
├── style.css
└── types.d.ts
├── tsconfig.json
├── webpack.config.js
├── webpack.dev.js
└── yarn.lock
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ich0x00/react-gantt-elastic/02afcf40823e7fc2ec453e209aca0eb467be90a2/.DS_Store
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": "commonjs",
7 | "useBuiltIns": "usage",
8 | "corejs": 3
9 | }
10 | ],
11 | "@babel/preset-react",
12 | "@babel/preset-typescript"
13 | ],
14 | "plugins": [
15 | "@babel/plugin-transform-runtime",
16 | // "@babel/plugin-proposal-class-properties",
17 | // "@babel/plugin-syntax-dynamic-import",
18 | "@babel/plugin-proposal-optional-chaining",
19 | "@babel/plugin-proposal-nullish-coalescing-operator"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /scripts
2 | /config
3 | /tests
4 | /src/serviceWorker.ts
5 | /webpack.config.js
6 | /webpack.dev.js
7 | /jest.config.js
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "sourceType": "module",
5 | "ecmaVersion": 2018,
6 | "allowImportExportEverywhere": true,
7 | "project": "./tsconfig.json"
8 | },
9 | "env": {
10 | "node": true,
11 | "es6": true,
12 | "mocha": true,
13 | "jest": true,
14 | "jasmine": true,
15 | "browser": true,
16 | "commonjs": true
17 | },
18 | "extends": [
19 | "eslint:recommended",
20 | "plugin:@typescript-eslint/eslint-recommended",
21 | "plugin:@typescript-eslint/recommended",
22 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
23 | "plugin:react/recommended"
24 | ],
25 | "settings": {
26 | "react": {
27 | "version": "detect"
28 | }
29 | },
30 | "plugins": ["react", "react-hooks", "@typescript-eslint"],
31 | "globals": {
32 | "__static": true
33 | },
34 | "rules": {
35 | "indent": 0,
36 | "linebreak-style": ["error", "unix"],
37 | "quotes": 0,
38 | // "quotes": ["error", "double"],
39 | "semi": ["error", "always"],
40 | "no-console": 0,
41 | "no-debugger": 0,
42 | "no-process-env": "error",
43 | "no-alert": "error",
44 | "react/prop-types": 0, //防止在React组件定义中丢失props验证
45 | // "react/no-unescaped-entities": 0,
46 | "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
47 | "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
48 | // "@typescript-eslint/explicit-function-return-type": [
49 | // "warn",
50 | // {
51 | // "allowExpressions": true,
52 | // "allowTypedFunctionExpresxsions": true
53 | // }
54 | // ],
55 | // "@typescript-eslint/no-empty-function": 0,
56 | // "@typescript-eslint/unbound-method": 0
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "overrides": [
3 | {
4 | "files": [".prettierrc", ".babelrc", ".eslintrc"],
5 | "options": {
6 | "parser": "json"
7 | }
8 | }
9 | ],
10 | "singleQuote": false
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 neuronet.io
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Gantt-elastic(Alpha) - Javascript Gantt Chart(editable, responsive)
2 | Javascript Gantt Chart for React, jquery, vanilla js and other frameworks
3 |
4 |
5 |
6 |
7 |

8 |

9 |

10 |

11 |

12 |

13 |

14 |
15 |
16 |
17 | Keywords: [ gantt, javascript gantt, gantt chart,js gantt,react gantt,project manager,gantt project manager,responsive gantt ]
18 |
19 |
20 |
21 |
22 | 
23 | 
24 | 
25 |
26 | ## Gantt-elastic
27 |
28 | is a vue component but it could be used in other frameworks or even with jQuery (vue is kind of elastic and lightweight framework).
29 |
30 | [WIKI](https://github.com/neuronetio/gantt-elastic/wiki)
31 |
32 | [Vue Example](https://github.com/neuronetio/vue-gantt-elastic)
33 |
34 | ### Installation
35 |
36 | `npm install --save react-gantt-elastic` or download zip from github / clone repo
37 |
38 | ### Usage
39 |
40 | ```tsx
41 | import dayjs from "dayjs";
42 | import React from "react";
43 | import ReactDOM from "react-dom";
44 | import GanttElastic from "./GanttElastic";
45 | import "./style.css";
46 |
47 | function getDate(hours: number): number {
48 | return (
49 | dayjs()
50 | .startOf("day")
51 | .valueOf() +
52 | hours * 60 * 60 * 1000
53 | );
54 | }
55 |
56 | const tasks = [
57 | {
58 | id: 1,
59 | label: "Make some noise",
60 | user:
61 | 'John Doe',
62 | start: getDate(-24 * 5),
63 | duration: 5 * 24 * 60 * 60 * 1000,
64 | progress: 85,
65 | type: "project"
66 | },
67 | {
68 | id: 2,
69 | label: "With great power comes great responsibility",
70 | user:
71 | 'Peter Parker',
72 | parentId: 1,
73 | start: getDate(-24 * 4),
74 | duration: 4 * 24 * 60 * 60 * 1000,
75 | progress: 50,
76 | type: "milestone",
77 | style: {
78 | base: {
79 | fill: "#1EBC61",
80 | stroke: "#0EAC51"
81 | }
82 | /*'tree-row-bar': {
83 | fill: '#1EBC61',
84 | stroke: '#0EAC51'
85 | },
86 | 'tree-row-bar-polygon': {
87 | stroke: '#0EAC51'
88 | }*/
89 | }
90 | },
91 | {
92 | id: 3,
93 | label: "Courage is being scared to death, but saddling up anyway.",
94 | user:
95 | 'John Wayne',
96 | parentId: 2,
97 | start: getDate(-24 * 3),
98 | duration: 2 * 24 * 60 * 60 * 1000,
99 | progress: 100,
100 | type: "task"
101 | },
102 | {
103 | id: 4,
104 | label: "Put that toy AWAY!",
105 | user:
106 | 'Clark Kent',
107 | start: getDate(-24 * 2),
108 | duration: 2 * 24 * 60 * 60 * 1000,
109 | progress: 50,
110 | type: "task",
111 | dependentOn: [3]
112 | },
113 | {
114 | id: 5,
115 | label:
116 | "One billion, gajillion, fafillion... shabadylu...mil...shabady......uh, Yen.",
117 | user:
118 | 'Austin Powers',
119 | parentId: 4,
120 | start: getDate(0),
121 | duration: 2 * 24 * 60 * 60 * 1000,
122 | progress: 10,
123 | type: "milestone",
124 | style: {
125 | base: {
126 | fill: "#0287D0",
127 | stroke: "#0077C0"
128 | }
129 | }
130 | },
131 | {
132 | id: 6,
133 | label: "Butch Mario and the Luigi Kid",
134 | user:
135 | 'Mario Bros',
136 | parentId: 5,
137 | start: getDate(24),
138 | duration: 1 * 24 * 60 * 60 * 1000,
139 | progress: 50,
140 | type: "task",
141 | style: {
142 | base: {
143 | fill: "#8E44AD",
144 | stroke: "#7E349D"
145 | }
146 | }
147 | },
148 | {
149 | id: 7,
150 | label: "Devon, the old man wanted me, it was his dying request",
151 | user:
152 | 'Knight Rider',
153 | parentId: 2,
154 | dependentOn: [6],
155 | start: getDate(24 * 2),
156 | duration: 4 * 60 * 60 * 1000,
157 | progress: 20,
158 | type: "task"
159 | },
160 | {
161 | id: 8,
162 | label: "Hey, Baby! Anybody ever tell you I have beautiful eyes?",
163 | user:
164 | 'Johhny Bravo',
165 | parentId: 7,
166 | dependentOn: [7],
167 | start: getDate(24 * 3),
168 | duration: 1 * 24 * 60 * 60 * 1000,
169 | progress: 0,
170 | type: "task"
171 | },
172 | {
173 | id: 9,
174 | label:
175 | "This better be important, woman. You are interrupting my very delicate calculations.",
176 | user:
177 | 'Dexter\'s Laboratory',
178 | parentId: 8,
179 | dependentOn: [8, 7],
180 | start: getDate(24 * 4),
181 | duration: 4 * 60 * 60 * 1000,
182 | progress: 20,
183 | type: "task",
184 | style: {
185 | base: {
186 | fill: "#8E44AD",
187 | stroke: "#7E349D"
188 | }
189 | }
190 | },
191 | {
192 | id: 10,
193 | label: "current task",
194 | user: (
195 |
200 | Johnattan Owens
201 |
202 | ),
203 | start: getDate(24 * 5),
204 | duration: 24 * 60 * 60 * 1000,
205 | progress: 0,
206 | type: "task"
207 | }
208 | ];
209 |
210 | const options = {
211 | title: {
212 | label: "Your project title as html (link or whatever...)",
213 | html: false
214 | },
215 | times: {
216 | timeZoom: 10,
217 | firstTime: dayjs("2020/03/10").valueOf()
218 | },
219 | row: { height: 16 },
220 | taskList: {
221 | columns: [
222 | {
223 | id: 1,
224 | label: "ID",
225 | value: "id",
226 | width: 40
227 | },
228 | {
229 | id: 2,
230 | label: "Description",
231 | value: "label",
232 | width: 200,
233 | expander: true
234 | },
235 | {
236 | id: 3,
237 | label: "Assigned to",
238 | value: "user",
239 | width: 130,
240 | html: true
241 | },
242 | {
243 | id: 4,
244 | label: "Start",
245 | value: task => dayjs(task.start).format("YYYY-MM-DD"),
246 | width: 78
247 | },
248 | {
249 | id: 5,
250 | label: "Type",
251 | value: "type",
252 | width: 68
253 | },
254 | {
255 | id: 6,
256 | label: "%",
257 | value: "progress",
258 | width: 35,
259 | style: {
260 | "task-list-header-label": {
261 | textAlign: "center",
262 | width: "100%"
263 | },
264 | "task-list-item-value-container": {
265 | textAlign: "center"
266 | }
267 | }
268 | }
269 | ]
270 | }
271 | // locale: {
272 | // name: "pl", // name String
273 | // weekdays: "Poniedziałek_Wtorek_Środa_Czwartek_Piątek_Sobota_Niedziela".split(
274 | // "_"
275 | // ), // weekdays Array
276 | // weekdaysShort: "Pon_Wto_Śro_Czw_Pią_Sob_Nie".split("_"), // OPTIONAL, short weekdays Array, use first three letters if not provided
277 | // weekdaysMin: "Pn_Wt_Śr_Cz_Pt_So_Ni".split("_"), // OPTIONAL, min weekdays Array, use first two letters if not provided
278 | // months: "Styczeń_Luty_Marzec_Kwiecień_Maj_Czerwiec_Lipiec_Sierpień_Wrzesień_Październik_Listopad_Grudzień".split(
279 | // "_"
280 | // ), // months Array
281 | // monthsShort: "Sty_Lut_Mar_Kwi_Maj_Cze_Lip_Sie_Wrz_Paź_Lis_Gru".split("_"), // OPTIONAL, short months Array, use first three letters if not provided
282 | // ordinal: n => `${n}`, // ordinal Function (number) => return number + output
283 | // relativeTime: {
284 | // // relative time format strings, keep %s %d as the same
285 | // future: "za %s", // e.g. in 2 hours, %s been replaced with 2hours
286 | // past: "%s temu",
287 | // s: "kilka sekund",
288 | // m: "minutę",
289 | // mm: "%d minut",
290 | // h: "godzinę",
291 | // hh: "%d godzin", // e.g. 2 hours, %d been replaced with 2
292 | // d: "dzień",
293 | // dd: "%d dni",
294 | // M: "miesiąc",
295 | // MM: "%d miesięcy",
296 | // y: "rok",
297 | // yy: "%d lat"
298 | // }
299 | // }
300 | };
301 |
302 | ReactDOM.render(
303 | ,
310 | document.getElementById("root")
311 | );
312 | ```
313 |
314 | ### Licensce
315 |
316 | MIT
317 |
--------------------------------------------------------------------------------
/examples/from-readme.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | GanttElastic editor demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
250 |
251 |
252 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Gangtt-elastic demo
7 |
8 |
9 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
303 |
304 |
305 |
--------------------------------------------------------------------------------
/examples/vue.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | GanttElastic demo
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
295 |
296 |
297 |
--------------------------------------------------------------------------------
/examples/vue.max-rows.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | GanttElastic editor demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | modify task
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
40 |
41 |
42 |
43 |
44 |
229 |
230 |
231 |
--------------------------------------------------------------------------------
/gantt-elastic.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ich0x00/react-gantt-elastic/02afcf40823e7fc2ec453e209aca0eb467be90a2/gantt-elastic.gif
--------------------------------------------------------------------------------
/gantt-elastic.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ich0x00/react-gantt-elastic/02afcf40823e7fc2ec453e209aca0eb467be90a2/gantt-elastic.jpg
--------------------------------------------------------------------------------
/grab-scroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ich0x00/react-gantt-elastic/02afcf40823e7fc2ec453e209aca0eb467be90a2/grab-scroll.gif
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // jest.config.js
2 | const { defaults } = require("jest-config");
3 |
4 | module.exports = {
5 | ...defaults,
6 | rootDir: "tests",
7 | verbose: true
8 | // moduleFileExtensions: [...defaults.moduleFileExtensions]
9 | };
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-gantt-elastic",
3 | "version": "1.0.1",
4 | "description": "Gantt chart. Elastic javascript gantt chart. React gantt. Project manager responsive gantt. jquery gantt.",
5 | "main": "src/GanttElastic.tsx",
6 | "dependencies": {
7 | "dayjs": "^1.8.20",
8 | "events": "^3.1.0",
9 | "react": "^16.12.0",
10 | "react-dom": "^16.12.0",
11 | "resize-observer-polyfill": "^1.5.1",
12 | "seamless-immutable": "^7.1.4",
13 | "ts-invariant": "^0.4.4"
14 | },
15 | "devDependencies": {
16 | "@babel/core": "^7.8.4",
17 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
18 | "@babel/plugin-proposal-optional-chaining": "^7.8.3",
19 | "@babel/plugin-transform-runtime": "^7.8.3",
20 | "@babel/preset-env": "^7.8.7",
21 | "@babel/preset-react": "^7.8.3",
22 | "@babel/preset-typescript": "^7.8.3",
23 | "@babel/runtime": "^7.8.4",
24 | "@types/lodash": "^4.14.149",
25 | "@types/react": "^16.9.22",
26 | "@types/react-dom": "^16.9.5",
27 | "@types/seamless-immutable": "^7.1.11",
28 | "@typescript-eslint/eslint-plugin": "^2.20.0",
29 | "@typescript-eslint/parser": "^2.21.0",
30 | "awesome-typescript-loader": "^5.2.1",
31 | "babel-eslint": "^10.0.3",
32 | "babel-jest": "^25.1.0",
33 | "babel-loader": "^8.0.6",
34 | "babel-plugin-lodash": "^3.3.4",
35 | "css-loader": "^3.4.2",
36 | "eslint": "^6.8.0",
37 | "eslint-plugin-react": "^7.18.3",
38 | "eslint-plugin-react-hooks": "^2.5.0",
39 | "html-webpack-plugin": "4.0.0-beta.11",
40 | "jest": "^25.1.0",
41 | "lodash-webpack-plugin": "^0.11.5",
42 | "mini-css-extract-plugin": "^0.9.0",
43 | "react-dev-utils": "^10.2.0",
44 | "react-test-renderer": "^16.13.0",
45 | "style-loader": "^1.1.3",
46 | "typescript": "^3.8.2",
47 | "webpack": "^4.41.6",
48 | "webpack-cli": "^3.3.11",
49 | "webpack-dev-server": "^3.10.3"
50 | },
51 | "scripts": {
52 | "test": "jest",
53 | "xtest": "npx cypress run --browser chrome",
54 | "build": "webpack",
55 | "dev": "webpack-dev-server --config webpack.dev.js --watch"
56 | },
57 | "repository": {
58 | "type": "git",
59 | "url": "git+https://github.com/bignumlock/react-gantt-elastic.git"
60 | },
61 | "keywords": [
62 | "gantt",
63 | "gantt js",
64 | "gantt chart",
65 | "gantt elastic",
66 | "javascript gantt",
67 | "javascript gantt chart",
68 | "jQuery gantt",
69 | "react gantt",
70 | "js gantt",
71 | "gantt-chart",
72 | "responsive",
73 | "react",
74 | "react-gantt",
75 | "javascript gantt",
76 | "project gantt",
77 | "project manger",
78 | "responsive gantt",
79 | "gantt component",
80 | "javascript gantt",
81 | "project",
82 | "task"
83 | ],
84 | "author": "ich ",
85 | "license": "MIT",
86 | "bugs": {
87 | "url": "https://github.com/bignumlock/react-gantt-elastic/issues"
88 | },
89 | "homepage": "https://github.com/bignumlock/react-gantt-elastic",
90 | "postcss": {
91 | "plugins": {
92 | "autoprefixer": {}
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ich0x00/react-gantt-elastic/02afcf40823e7fc2ec453e209aca0eb467be90a2/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ich0x00/react-gantt-elastic/02afcf40823e7fc2ec453e209aca0eb467be90a2/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ich0x00/react-gantt-elastic/02afcf40823e7fc2ec453e209aca0eb467be90a2/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/GanttElasticContext.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/no-empty-function */
3 | import React from "react";
4 | import Immutable, { ImmutableObject } from "seamless-immutable";
5 | import {
6 | CalendarOption,
7 | GanttElasticRefs,
8 | Options,
9 | ScrollOption,
10 | Task,
11 | TaskListOption,
12 | TimeOption
13 | } from "./components/interfaces";
14 | import { defaultOptions, defaultState } from "./components/utils/options";
15 | import { DynamicStyle } from "./types";
16 |
17 | export default React.createContext<{
18 | dispatch?: React.Dispatch<{ type: string; payload: any }>;
19 | isTaskVisible?: (task: Task) => boolean;
20 | getTask?: (taskId: string | number) => Task;
21 | ctx?: CanvasRenderingContext2D | null;
22 | refs: GanttElasticRefs;
23 | style: DynamicStyle;
24 | allTasks: Task[];
25 | visibleTasks: Task[];
26 | // calendar: ImmutableObject;
27 | options: ImmutableObject;
28 | scroll: ImmutableObject;
29 | times: ImmutableObject;
30 | taskList: ImmutableObject;
31 | calendar: ImmutableObject;
32 | chartWidth: number;
33 | clientHeight: number;
34 | clientWidth: number;
35 | outerHeight: number;
36 | rowsHeight: number;
37 | scrollBarHeight: number;
38 | allVisibleTasksHeight: number;
39 | }>({
40 | refs: {},
41 | allTasks: [],
42 | visibleTasks: [],
43 | style: {},
44 | chartWidth: 0,
45 | clientHeight: 0,
46 | clientWidth: 0,
47 | outerHeight: 0,
48 | rowsHeight: 0,
49 | scrollBarHeight: 0,
50 | allVisibleTasksHeight: 0,
51 |
52 | options: Immutable(defaultOptions),
53 | ...Immutable(defaultState)
54 | });
55 |
--------------------------------------------------------------------------------
/src/GanttElasticEvents.ts:
--------------------------------------------------------------------------------
1 | import events from "events";
2 |
3 | const emitEvent = new events.EventEmitter();
4 |
5 | export { emitEvent };
6 |
--------------------------------------------------------------------------------
/src/components/Chart/Calendar/Calendar.tsx:
--------------------------------------------------------------------------------
1 | import { CalendarRowItems, CalendarRowText } from "@/components/interfaces";
2 | import {
3 | calculateCalendarDimensions,
4 | getMonthsCount,
5 | howManyDaysFit,
6 | howManyHoursFit,
7 | howManyMonthsFit
8 | } from "@/components/utils/times";
9 | import GanttElasticContext from "@/GanttElasticContext";
10 | import dayjs from "dayjs";
11 | import _ from "lodash";
12 | import React, {
13 | useContext,
14 | useEffect,
15 | useLayoutEffect,
16 | useMemo,
17 | useRef
18 | } from "react";
19 | import invariant from "ts-invariant";
20 | import CalendarRow from "./CalendarRow";
21 |
22 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
23 | export interface CalendarProps {}
24 |
25 | export interface CalendarState {
26 | hours: CalendarRowItems[];
27 | days: CalendarRowItems[];
28 | months: CalendarRowItems[];
29 | }
30 |
31 | const Calendar: React.FC = () => {
32 | const {
33 | refs,
34 | style,
35 | dispatch,
36 | options,
37 | chartWidth,
38 | times,
39 | calendar
40 | } = useContext(GanttElasticContext);
41 |
42 | // refs
43 | const chartCalendarContainerRef = useRef(null);
44 |
45 | useEffect(() => {
46 | refs.chartCalendarContainer = chartCalendarContainerRef;
47 | }, [refs]);
48 |
49 | /**
50 | * Generate hours
51 | *
52 | * @returns {array}
53 | */
54 | const hours = useMemo(() => {
55 | const allHours: CalendarRowItems[] = [];
56 |
57 | if (options.calendar.hour.display) {
58 | const { widths, maxWidths, formatted } = calendar.hour;
59 | const dayRowHeight = options.calendar.day.height;
60 | const monthRowHeight = options.calendar.month.height;
61 | const rowHeight = options.calendar.hour.height;
62 | const format = options.calendar.hour.format;
63 | const steps = times.steps;
64 | for (
65 | let hourIndex = 0, len = steps.length;
66 | hourIndex < len;
67 | hourIndex++
68 | ) {
69 | const hoursCount = howManyHoursFit(
70 | format,
71 | steps[hourIndex].width.px,
72 | maxWidths
73 | );
74 | if (hoursCount.count === 0) {
75 | continue;
76 | }
77 | const hours: CalendarRowItems = {
78 | key: hourIndex + "step",
79 | children: []
80 | };
81 | const hourStep = 24 / hoursCount.count;
82 | const hourWidthPx = steps[hourIndex].width.px / hoursCount.count;
83 | for (let i = 0, len = hoursCount.count; i < len; i++) {
84 | const hour = i * hourStep;
85 | let index = hourIndex;
86 | if (hourIndex > 0) {
87 | index = hourIndex - Math.floor(hourIndex / 24) * 24;
88 | }
89 | let textWidth = 0;
90 | if (typeof widths[index] !== "undefined") {
91 | textWidth = widths[index][hoursCount.type];
92 | }
93 | const x = steps[hourIndex].offset.px + hourWidthPx * i;
94 |
95 | hours.children.push({
96 | index: hourIndex,
97 | key: "h" + i,
98 | x,
99 | y: dayRowHeight + monthRowHeight,
100 | width: hourWidthPx,
101 | textWidth,
102 | height: rowHeight,
103 | label: formatted[hoursCount.type][hour]
104 | });
105 | }
106 | allHours.push(hours);
107 | }
108 | }
109 | return allHours;
110 | }, [
111 | calendar.hour,
112 | options.calendar.day.height,
113 | options.calendar.hour.display,
114 | options.calendar.hour.format,
115 | options.calendar.hour.height,
116 | options.calendar.month.height,
117 | times.steps
118 | ]);
119 |
120 | /**
121 | * Generate days
122 | *
123 | * @returns {array}
124 | */
125 | const days = useMemo(() => {
126 | let allDays: CalendarRowItems[] = [];
127 |
128 | if (options.calendar.day.display) {
129 | const { widths, maxWidths } = calendar.day;
130 | const monthRowHeight = options.calendar.month.height;
131 | const rowHeight = options.calendar.day.height;
132 | const format = options.calendar.day.format;
133 | const steps = times.steps;
134 |
135 | const localeName = options.locale.name;
136 |
137 | const days: CalendarRowText[] = [];
138 | const daysCount = howManyDaysFit(
139 | format,
140 | chartWidth,
141 | maxWidths,
142 | steps.length
143 | );
144 | if (daysCount.count === 0) {
145 | return allDays;
146 | }
147 |
148 | const dayStep = Math.ceil(steps.length / daysCount.count);
149 | for (
150 | let dayIndex = 0, len = steps.length;
151 | dayIndex < len;
152 | dayIndex += dayStep
153 | ) {
154 | let dayWidthPx = 0;
155 | // day could be shorter (daylight saving time) so join widths and divide
156 | for (let currentStep = 0; currentStep < dayStep; currentStep++) {
157 | if (typeof steps[dayIndex + currentStep] !== "undefined") {
158 | dayWidthPx += steps[dayIndex + currentStep].width.px;
159 | }
160 | }
161 | const date = dayjs(steps[dayIndex].time);
162 | let textWidth = 0;
163 | if (typeof widths[dayIndex] !== "undefined") {
164 | textWidth = widths[dayIndex][daysCount.type];
165 | }
166 | const x = steps[dayIndex].offset.px;
167 | days.push({
168 | index: dayIndex,
169 | key: steps[dayIndex].time + "d",
170 | x,
171 | y: monthRowHeight,
172 | width: dayWidthPx,
173 | textWidth,
174 | height: rowHeight,
175 | label: format[daysCount.type](date.locale(localeName))
176 | });
177 | }
178 | allDays = _.map(days, item => ({
179 | key: item.key,
180 | children: [item]
181 | }));
182 | }
183 | return allDays;
184 | }, [
185 | calendar.day,
186 | chartWidth,
187 | options.calendar.day.display,
188 | options.calendar.day.format,
189 | options.calendar.day.height,
190 | options.calendar.month.height,
191 | options.locale.name,
192 | times.steps
193 | ]);
194 |
195 | /**
196 | * Generate months
197 | *
198 | * @returns {array}
199 | */
200 | const months = useMemo(() => {
201 | let allMonths: CalendarRowItems[] = [];
202 | if (options.calendar.month.display) {
203 | const { widths, maxWidths } = calendar.month;
204 | const rowHeight = options.calendar.month.height;
205 | const format = options.calendar.month.format;
206 | const steps = times.steps;
207 | const firstTime = times.firstTime;
208 | const lastTime = times.lastTime;
209 | const localeName = options.locale.name;
210 |
211 | const months: CalendarRowText[] = [];
212 |
213 | const count = getMonthsCount(firstTime, lastTime);
214 | const monthsCount = howManyMonthsFit(
215 | format,
216 | chartWidth,
217 | maxWidths,
218 | count
219 | );
220 | if (monthsCount.count === 0) {
221 | return allMonths;
222 | }
223 | let currentDate = dayjs(firstTime);
224 | const _lastTime = dayjs(lastTime);
225 | for (let monthIndex = 0; monthIndex < monthsCount.count; monthIndex++) {
226 | let monthWidth = 0;
227 | let monthOffset = Number.MAX_SAFE_INTEGER;
228 | let finalDate = dayjs(currentDate)
229 | .add(1, "month")
230 | .startOf("month");
231 | if (finalDate.valueOf() > _lastTime.valueOf()) {
232 | finalDate = _lastTime;
233 | }
234 | // we must find first and last step to get the offsets / widths
235 | for (let step = 0, len = steps.length; step < len; step++) {
236 | const currentStep = steps[step];
237 | if (
238 | currentStep.time >= currentDate.valueOf() &&
239 | currentStep.time < finalDate.valueOf()
240 | ) {
241 | monthWidth += currentStep.width.px;
242 | if (currentStep.offset.px < monthOffset) {
243 | monthOffset = currentStep.offset.px;
244 | }
245 | }
246 | }
247 | let label = "";
248 | let choosenFormatName;
249 | for (const formatName in format) {
250 | if (maxWidths[formatName] + 2 <= monthWidth) {
251 | label = format[formatName](currentDate.locale(localeName));
252 | choosenFormatName = formatName;
253 | }
254 | }
255 | let textWidth = 0;
256 | if (typeof widths[monthIndex] !== "undefined" && choosenFormatName) {
257 | textWidth = widths[monthIndex][choosenFormatName];
258 | }
259 | const x = monthOffset;
260 | months.push({
261 | index: monthIndex,
262 | key: monthIndex + "m",
263 | x,
264 | y: 0,
265 | width: monthWidth,
266 | textWidth,
267 | choosenFormatName,
268 | height: rowHeight,
269 | label
270 | });
271 | currentDate = currentDate.add(1, "month").startOf("month");
272 | if (currentDate.valueOf() > _lastTime.valueOf()) {
273 | currentDate = _lastTime;
274 | }
275 | }
276 | allMonths = _.map(months, item => ({
277 | key: item.key,
278 | children: [item]
279 | }));
280 | }
281 | return allMonths;
282 | }, [
283 | calendar.month,
284 | chartWidth,
285 | options.calendar.month.display,
286 | options.calendar.month.format,
287 | options.calendar.month.height,
288 | options.locale.name,
289 | times.firstTime,
290 | times.lastTime,
291 | times.steps
292 | ]);
293 |
294 | useLayoutEffect(() => {
295 | const height = calculateCalendarDimensions(
296 | hours,
297 | days,
298 | months,
299 | options.asMutable({ deep: true })
300 | );
301 | if (calendar.height !== height) {
302 | invariant.warn(`set calendar's height:${height}`);
303 | dispatch &&
304 | dispatch({
305 | type: "update-calendar-height",
306 | payload: height
307 | });
308 | }
309 | }, [calendar.height, days, dispatch, hours, months, options]);
310 |
311 | return useMemo(
312 | () => (
313 |
320 |
327 | {options.calendar.month.display && (
328 |
329 | )}
330 | {options.calendar.day.display && (
331 |
332 | )}
333 | {options.calendar.hour.display && (
334 |
335 | )}
336 |
337 |
338 | ),
339 | [
340 | days,
341 | hours,
342 | months,
343 | chartWidth,
344 | options.calendar.day.display,
345 | options.calendar.hour.display,
346 | options.calendar.month.display,
347 | style
348 | ]
349 | );
350 | };
351 |
352 | export default Calendar;
353 |
--------------------------------------------------------------------------------
/src/components/Chart/Calendar/CalendarRow.tsx:
--------------------------------------------------------------------------------
1 | import { CalendarRowItems } from "@/components/interfaces";
2 | import GanttElasticContext from "@/GanttElasticContext";
3 | import _ from "lodash";
4 | import React, { useContext, useMemo } from "react";
5 | import RowText from "./RowText";
6 |
7 | interface CalendarRowProps {
8 | items: Array;
9 | which: string;
10 | }
11 |
12 | const CalendarRow: React.FC = ({ items, which }) => {
13 | const { style } = useContext(GanttElasticContext);
14 |
15 | return useMemo(() => {
16 | const rowStyle = {
17 | ...style["calendar-row"],
18 | ...style["calendar-row--" + which]
19 | };
20 |
21 | const rectStyle = {
22 | ...style["calendar-row-rect"],
23 | ...style["calendar-row-rect--" + which]
24 | };
25 |
26 | return (
27 |
33 | {_.map(items, item => (
34 |
42 | {_.map(item.children, child => (
43 |
44 | ))}
45 |
46 | ))}
47 |
48 | );
49 | }, [items, style, which]);
50 | };
51 |
52 | export default CalendarRow;
53 |
--------------------------------------------------------------------------------
/src/components/Chart/Calendar/RowText.tsx:
--------------------------------------------------------------------------------
1 | import { CalendarRowText } from "@/components/interfaces";
2 | import { isInsideViewPort } from "@/components/utils/charts";
3 | import GanttElasticContext from "@/GanttElasticContext";
4 | import React, { useContext, useMemo } from "react";
5 |
6 | export interface CalendarRowTextProps {
7 | item: CalendarRowText;
8 | which: string;
9 | }
10 |
11 | const RowText: React.FC = ({ item, which }) => {
12 | const { style, scroll } = useContext(GanttElasticContext);
13 |
14 | return useMemo(() => {
15 | const rectChildStyle = {
16 | ...style["calendar-row-rect-child"],
17 | ...style["calendar-row-rect-child--" + which],
18 | width: item.width + "px",
19 | height: item.height + "px"
20 | };
21 |
22 | const textStyle = {
23 | ...style["calendar-row-text"],
24 | ...style["calendar-row-text--" + which]
25 | };
26 |
27 | if (which === "month") {
28 | let x = item.x + item.width / 2 - item.textWidth / 2;
29 | if (
30 | which === "month" &&
31 | isInsideViewPort(
32 | scroll.chart.left,
33 | scroll.chart.right,
34 | item.x,
35 | item.width,
36 | 0
37 | )
38 | ) {
39 | const scrollWidth = scroll.chart.right - scroll.chart.left;
40 | x = scroll.chart.left + scrollWidth / 2 - item.textWidth / 2 + 2;
41 | if (x + item.textWidth + 2 > item.x + item.width) {
42 | x = item.x + item.width - item.textWidth - 2;
43 | } else if (x < item.x) {
44 | x = item.x + 2;
45 | }
46 | }
47 | textStyle.left = x - item.x + "px";
48 | }
49 |
50 | return (
51 |
58 |
65 | {item.label}
66 |
67 |
68 | );
69 | }, [
70 | item.height,
71 | item.label,
72 | item.textWidth,
73 | item.width,
74 | item.x,
75 | scroll.chart.left,
76 | scroll.chart.right,
77 | style,
78 | which
79 | ]);
80 | };
81 |
82 | export default RowText;
83 |
--------------------------------------------------------------------------------
/src/components/Chart/Chart.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import _ from "lodash";
3 | import React, { useContext, useEffect, useMemo, useRef } from "react";
4 | import Calendar from "./Calendar/Calendar";
5 | import DaysHighlight from "./DaysHighlight";
6 | import DependencyLines from "./DependencyLines";
7 | import Grid from "./Grid";
8 | import Milestone from "./Row/Milestone";
9 | import Project from "./Row/Project";
10 | import Task from "./Row/Task";
11 |
12 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
13 | export interface ChartProps {}
14 |
15 | const Chart: React.FC = () => {
16 | const {
17 | style,
18 | visibleTasks,
19 | refs,
20 | calendar,
21 | clientHeight,
22 | chartWidth,
23 | rowsHeight,
24 | allVisibleTasksHeight,
25 | options
26 | } = useContext(GanttElasticContext);
27 |
28 | const chartRef = useRef(null);
29 | const chartCalendarContainerRef = useRef(null);
30 | const chartGraphContainerRef = useRef(null);
31 | const chartGraphRef = useRef(null);
32 | const chartGraphSvgRef = useRef(null);
33 |
34 | useEffect(() => {
35 | refs.chart = chartRef;
36 | refs.chartCalendarContainer = chartCalendarContainerRef;
37 | refs.chartGraphContainer = chartGraphContainerRef;
38 | refs.chartGraph = chartGraphRef;
39 | refs.chartGraphSvg = chartGraphSvgRef;
40 | }, [refs]);
41 |
42 | const renderTasks = useMemo(() => {
43 | return (
44 |
75 | );
76 | }, [style, chartWidth, allVisibleTasksHeight, visibleTasks]);
77 |
78 | return useMemo(
79 | () => (
80 |
85 |
94 |
95 |
96 |
104 |
111 |
119 | {renderTasks}
120 |
121 |
122 |
123 |
124 | ),
125 | [
126 | calendar.height,
127 | clientHeight,
128 | options.calendar.gap,
129 | renderTasks,
130 | rowsHeight,
131 | style,
132 | chartWidth
133 | ]
134 | );
135 | };
136 |
137 | export default Chart;
138 |
--------------------------------------------------------------------------------
/src/components/Chart/DaysHighlight.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import dayjs from "dayjs";
3 | import _ from "lodash";
4 | import React, { useContext, useMemo } from "react";
5 |
6 | /**
7 | * Show working days?
8 | *
9 | * @returns {bool}
10 | */
11 | const showWorkingDays = (workingDays: number[]): boolean => {
12 | if (
13 | typeof workingDays !== "undefined" &&
14 | Array.isArray(workingDays) &&
15 | workingDays.length
16 | ) {
17 | return true;
18 | }
19 | return false;
20 | };
21 |
22 | /**
23 | * Get key
24 | *
25 | * @param {object} day
26 | * @returns {string} key ideintifier for loop
27 | */
28 | function getKey(
29 | time: string | number | Date | dayjs.Dayjs | undefined
30 | ): string {
31 | return dayjs(time).format("YYYY-MM-DD");
32 | }
33 |
34 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
35 | export interface DaysHighlightProps {}
36 |
37 | const DaysHighlight: React.FC = () => {
38 | const { style, options, times } = useContext(GanttElasticContext);
39 |
40 | const { workingDays } = options.calendar;
41 |
42 | return useMemo(() => {
43 | /**
44 | * Get working days
45 | */
46 | const workingSteps = _.filter(times.steps, step => {
47 | return !workingDays.includes(dayjs(step.time).day());
48 | });
49 |
50 | return (
51 |
55 | {showWorkingDays(workingDays.asMutable()) &&
56 | _.map(workingSteps, day => (
57 |
68 | ))}
69 |
70 | );
71 | }, [style, times.steps, workingDays]);
72 | };
73 |
74 | export default DaysHighlight;
75 |
--------------------------------------------------------------------------------
/src/components/Chart/DependencyLines.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import _ from "lodash";
3 | import React, { useContext, useMemo } from "react";
4 | import { Task } from "../interfaces";
5 |
6 | /**
7 | * Get path points
8 | */
9 | const getPoints = (
10 | fromTaskId: string | number,
11 | toTaskId: string | number,
12 | isTaskVisible?: (task: Task) => boolean,
13 | getTask?: (id: string | number) => Task
14 | ): string | undefined => {
15 | if (getTask && isTaskVisible) {
16 | const fromTask = getTask(fromTaskId);
17 | const toTask = getTask(toTaskId);
18 | if (
19 | fromTask === null ||
20 | toTask === null ||
21 | !isTaskVisible(toTask) ||
22 | !isTaskVisible(fromTask)
23 | ) {
24 | return;
25 | }
26 | const startX = fromTask.x + fromTask.width;
27 | const startY = fromTask.y + fromTask.height / 2;
28 | const stopX = toTask.x;
29 | const stopY = toTask.y + toTask.height / 2;
30 | const distanceX = stopX - startX;
31 | let distanceY;
32 | let yMultiplier = 1;
33 | if (stopY >= startY) {
34 | distanceY = stopY - startY;
35 | } else {
36 | distanceY = startY - stopY;
37 | yMultiplier = -1;
38 | }
39 | const offset = 10;
40 | const roundness = 4;
41 | const isBefore = distanceX <= offset + roundness;
42 | let points = `M ${startX} ${startY}
43 | L ${startX + offset},${startY} `;
44 | if (isBefore) {
45 | points += `Q ${startX + offset + roundness},${startY} ${startX +
46 | offset +
47 | roundness},${startY + roundness * yMultiplier}
48 | L ${startX + offset + roundness},${startY +
49 | (distanceY * yMultiplier) / 2 -
50 | roundness * yMultiplier}
51 | Q ${startX + offset + roundness},${startY +
52 | (distanceY * yMultiplier) / 2} ${startX + offset},${startY +
53 | (distanceY * yMultiplier) / 2}
54 | L ${startX - offset + distanceX},${startY +
55 | (distanceY * yMultiplier) / 2}
56 | Q ${startX - offset + distanceX - roundness},${startY +
57 | (distanceY * yMultiplier) / 2} ${startX -
58 | offset +
59 | distanceX -
60 | roundness},${startY +
61 | (distanceY * yMultiplier) / 2 +
62 | roundness * yMultiplier}
63 | L ${startX - offset + distanceX - roundness},${stopY -
64 | roundness * yMultiplier}
65 | Q ${startX - offset + distanceX - roundness},${stopY} ${startX -
66 | offset +
67 | distanceX},${stopY}
68 | L ${stopX},${stopY}`;
69 | } else {
70 | points += `L ${startX + distanceX / 2 - roundness},${startY}
71 | Q ${startX + distanceX / 2},${startY} ${startX +
72 | distanceX / 2},${startY + roundness * yMultiplier}
73 | L ${startX + distanceX / 2},${stopY - roundness * yMultiplier}
74 | Q ${startX + distanceX / 2},${stopY} ${startX +
75 | distanceX / 2 +
76 | roundness},${stopY}
77 | L ${stopX},${stopY}`;
78 | }
79 | return points;
80 | }
81 | };
82 |
83 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
84 | export interface DaysHighlightProps {}
85 |
86 | const DependencyLines: React.FC = () => {
87 | const { style, allTasks, isTaskVisible, getTask } = useContext(
88 | GanttElasticContext
89 | );
90 |
91 | /**
92 | * Get tasks which are dependent on other tasks
93 | *
94 | * @returns {array}
95 | */
96 | const dependencyTasks = useMemo(() => {
97 | return _.filter(allTasks, task => !task.dependentOn)
98 | .map(task => {
99 | task.dependencyLines = _.map(task.dependentOn, id => ({
100 | points: getPoints(id, task.id, isTaskVisible, getTask),
101 | taskId: id
102 | }));
103 | return task;
104 | })
105 | .filter(task => task.dependencyLines);
106 | }, [allTasks, isTaskVisible, getTask]);
107 |
108 | return useMemo(() => {
109 | return (
110 |
137 | );
138 | }, [dependencyTasks, style]);
139 | };
140 |
141 | export default DependencyLines;
142 |
--------------------------------------------------------------------------------
/src/components/Chart/Grid.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import _ from "lodash";
3 | import React, { useContext, useEffect, useMemo, useRef } from "react";
4 | import { isInsideViewPort, timeToPixelOffsetX } from "../utils/charts";
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
7 | export interface ChartProps {}
8 |
9 | const Grid: React.FC = () => {
10 | const {
11 | style,
12 | options,
13 | refs,
14 | visibleTasks,
15 | allTasks,
16 | times,
17 | scroll,
18 | chartWidth,
19 | allVisibleTasksHeight
20 | } = useContext(GanttElasticContext);
21 | const allTaskCount = allTasks.length;
22 | // refs
23 | const svgChartRef = useRef(null);
24 |
25 | useEffect(() => {
26 | refs.svgChart = svgChartRef;
27 | }, [refs]);
28 |
29 | const strokeWidth = useMemo(
30 | () => parseInt(`${style["grid-line-vertical"]["strokeWidth"]}`),
31 | [style]
32 | );
33 |
34 | /**
35 | * Generate vertical lines of the grid
36 | */
37 | const renderVerticalLines = useMemo(() => {
38 | const lines: JSX.Element[] = [];
39 | _.forEach(times.steps, step => {
40 | if (
41 | isInsideViewPort(
42 | scroll.chart.left,
43 | scroll.chart.right,
44 | step.offset.px,
45 | 1
46 | )
47 | ) {
48 | lines.push(
49 |
62 | );
63 | }
64 | });
65 | return {lines};
66 | }, [
67 | allTaskCount,
68 | options.chart.grid.horizontal.gap,
69 | options.row.height,
70 | scroll.chart.left,
71 | scroll.chart.right,
72 | strokeWidth,
73 | style,
74 | times.steps
75 | ]);
76 |
77 | /**
78 | * Generate horizontal lines of the grid
79 | */
80 | const renderHorizontalLines = useMemo(() => {
81 | return (
82 | <>
83 | {_.map(visibleTasks, (_: object, index: number) => {
84 | const y =
85 | index *
86 | (options.row.height + options.chart.grid.horizontal.gap * 2) +
87 | strokeWidth / 2;
88 |
89 | return (
90 |
99 | );
100 | })}
101 | >
102 | );
103 | }, [
104 | options.chart.grid.horizontal.gap,
105 | options.row.height,
106 | strokeWidth,
107 | style,
108 | visibleTasks
109 | ]);
110 |
111 | /**
112 | * Get current time line position
113 | *
114 | * @returns {object}
115 | */
116 | const timeLinePosition = useMemo(() => {
117 | const d = new Date();
118 | const current = d.getTime();
119 | const currentOffset = timeToPixelOffsetX(
120 | current,
121 | times.firstTime,
122 | options.times.timePerPixel
123 | );
124 | const timeLine = {
125 | x: 0,
126 | y1: 0,
127 | y2: "100%",
128 | dateTime: "",
129 | time: current
130 | };
131 | timeLine.x = currentOffset;
132 | timeLine.dateTime = d.toLocaleDateString();
133 | return timeLine;
134 | }, [options.times.timePerPixel, times.firstTime]);
135 |
136 | return useMemo(
137 | () => (
138 |
164 | ),
165 | [
166 | allVisibleTasksHeight,
167 | renderHorizontalLines,
168 | renderVerticalLines,
169 | style,
170 | timeLinePosition.x,
171 | timeLinePosition.y1,
172 | timeLinePosition.y2,
173 | chartWidth
174 | ]
175 | );
176 | };
177 |
178 | export default Grid;
179 |
--------------------------------------------------------------------------------
/src/components/Chart/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import { Task } from "@/components/interfaces";
2 | import GanttElasticContext from "@/GanttElasticContext";
3 | import React, { useContext, useMemo } from "react";
4 |
5 | export interface ProgressBarProps {
6 | task: Task;
7 | clipPath: string;
8 | }
9 |
10 | const ProgressBar: React.FC = ({ task, ...props }) => {
11 | const { style, options } = useContext(GanttElasticContext);
12 |
13 | return useMemo(() => {
14 | // Get progress width
15 | const progressWidth = task.progress + "%";
16 | // Get line points
17 | const start = (task.width / 100) * task.progress;
18 | const linePoints = `M ${start} 0 L ${start} ${task.height}`;
19 |
20 | return (
21 |
29 |
30 |
37 |
48 |
49 |
50 | {options.chart.progress.bar && (
51 |
61 | )}
62 | {options.chart.progress.pattern && (
63 |
64 |
75 |
84 |
85 | )}
86 |
87 | );
88 | }, [
89 | options.chart.progress.bar,
90 | options.chart.progress.pattern,
91 | options.chart.progress.width,
92 | style,
93 | task.height,
94 | task.progress,
95 | task.style,
96 | task.width
97 | ]);
98 | };
99 |
100 | export default ProgressBar;
101 |
--------------------------------------------------------------------------------
/src/components/Chart/Row/Milestone.tsx:
--------------------------------------------------------------------------------
1 | import Expander from "@/components/Expander";
2 | import { Task } from "@/components/interfaces";
3 | import GanttElasticContext from "@/GanttElasticContext";
4 | import { emitEvent } from "@/GanttElasticEvents";
5 | import React, { useCallback, useContext, useMemo } from "react";
6 | import { invariant } from "ts-invariant";
7 | import ProgressBar from "../ProgressBar";
8 | import ChartText from "../Text";
9 |
10 | export interface MilestoneProps {
11 | task: Task;
12 | }
13 |
14 | const ChartMilestone: React.FC = ({ task }) => {
15 | const { style, options } = useContext(GanttElasticContext);
16 |
17 | /**
18 | * Emit event
19 | *
20 | * @param {string} eventName
21 | * @param {Event} event
22 | */
23 | const onEmitEvent = useCallback(
24 | event => {
25 | invariant.warn(event.type, task);
26 | emitEvent.emit(`chart-${task.type}-${event.type}`, {
27 | event,
28 | data: task
29 | });
30 | task.events &&
31 | task.events[event.type] &&
32 | task.events[event.type](event, task);
33 | },
34 | [task]
35 | );
36 |
37 | /**
38 | * Get points
39 | */
40 | const points = useMemo(() => {
41 | const fifty = task.height / 2;
42 | let offset = fifty;
43 | if (task.width / 2 - offset < 0) {
44 | offset = task.width / 2;
45 | }
46 | return `0,${fifty}
47 | ${offset},0
48 | ${task.width - offset},0
49 | ${task.width},${fifty}
50 | ${task.width - offset},${task.height}
51 | ${offset},${task.height}`;
52 | }, [task.height, task.width]);
53 |
54 | return useMemo(() => {
55 | const clipPathId = `gantt-elastic__milestone-clip-path-${task.id}`;
56 |
57 | const displayExpander =
58 | options.chart.expander.display ||
59 | (options.chart.expander.displayIfTaskListHidden &&
60 | !options.taskList.display);
61 |
62 | return (
63 |
71 | {displayExpander && (
72 |
87 |
97 |
98 | )}
99 |
144 |
145 | {options.chart.text.display && }
146 |
147 | );
148 | }, [
149 | onEmitEvent,
150 | options.chart.expander.display,
151 | options.chart.expander.displayIfTaskListHidden,
152 | options.chart.expander.offset,
153 | options.chart.expander.size,
154 | options.chart.expander.type,
155 | options.chart.text.display,
156 | options.row.height,
157 | options.taskList.display,
158 | points,
159 | style,
160 | task
161 | ]);
162 | };
163 |
164 | export default ChartMilestone;
165 |
--------------------------------------------------------------------------------
/src/components/Chart/Row/Project.tsx:
--------------------------------------------------------------------------------
1 | import Expander from "@/components/Expander";
2 | import { Task } from "@/components/interfaces";
3 | import GanttElasticContext from "@/GanttElasticContext";
4 | import { emitEvent } from "@/GanttElasticEvents";
5 | import React, { useCallback, useContext, useMemo } from "react";
6 | import { invariant } from "ts-invariant";
7 | import ProgressBar from "../ProgressBar";
8 | import ChartText from "../Text";
9 |
10 | export interface ProjectProps {
11 | task: Task;
12 | }
13 |
14 | const ChartProject: React.FC = ({ task }) => {
15 | const { style, options } = useContext(GanttElasticContext);
16 |
17 | /**
18 | * Emit event
19 | *
20 | * @param {string} eventName
21 | * @param {Event} event
22 | */
23 | const onEmitEvent = useCallback(
24 | event => {
25 | invariant.warn(event.type, task);
26 | emitEvent.emit(`chart-${task.type}-${event.type}`, {
27 | event,
28 | data: task
29 | });
30 | task.events &&
31 | task.events[event.type] &&
32 | task.events[event.type](event, task);
33 | },
34 | [task]
35 | );
36 |
37 | /**
38 | * Get points
39 | */
40 | const points = useMemo(() => {
41 | const bottom = task.height - task.height / 4;
42 | const corner = task.height / 6;
43 | const smallCorner = task.height / 8;
44 | return `M ${smallCorner},0
45 | L ${task.width - smallCorner} 0
46 | L ${task.width} ${smallCorner}
47 | L ${task.width} ${bottom}
48 | L ${task.width - corner} ${task.height}
49 | L ${task.width - corner * 2} ${bottom}
50 | L ${corner * 2} ${bottom}
51 | L ${corner} ${task.height}
52 | L 0 ${bottom}
53 | L 0 ${smallCorner}
54 | Z
55 | `;
56 | }, [task.height, task.width]);
57 |
58 | return useMemo(() => {
59 | const clipPathId = `gantt-elastic__project-clip-path-${task.id}`;
60 |
61 | const displayExpander =
62 | options.chart.expander.display ||
63 | (options.chart.expander.displayIfTaskListHidden &&
64 | !options.taskList.display);
65 |
66 | return (
67 |
75 | {displayExpander && (
76 |
91 |
101 |
102 | )}
103 |
148 | {options.chart.text.display && }
149 |
150 | );
151 | }, [
152 | onEmitEvent,
153 | options.chart.expander.display,
154 | options.chart.expander.displayIfTaskListHidden,
155 | options.chart.expander.offset,
156 | options.chart.expander.size,
157 | options.chart.expander.type,
158 | options.chart.text.display,
159 | options.row.height,
160 | options.taskList.display,
161 | points,
162 | style,
163 | task
164 | ]);
165 | };
166 |
167 | export default ChartProject;
168 |
--------------------------------------------------------------------------------
/src/components/Chart/Row/Task.tsx:
--------------------------------------------------------------------------------
1 | import Expander from "@/components/Expander";
2 | import { Task } from "@/components/interfaces";
3 | import GanttElasticContext from "@/GanttElasticContext";
4 | import { emitEvent } from "@/GanttElasticEvents";
5 | import React, { useCallback, useContext, useMemo } from "react";
6 | import invariant from "ts-invariant";
7 | import ProgressBar from "../ProgressBar";
8 | import ChartText from "../Text";
9 |
10 | export interface TaskProps {
11 | task: Task;
12 | }
13 |
14 | const ChartTask: React.FC = ({ task }) => {
15 | const { style, options } = useContext(GanttElasticContext);
16 |
17 | /**
18 | * Emit event
19 | *
20 | * @param {string} eventName
21 | * @param {Event} event
22 | */
23 | const onEmitEvent = useCallback(
24 | event => {
25 | invariant.warn(event.type, task);
26 | emitEvent.emit(`chart-${task.type}-${event.type}`, {
27 | event,
28 | data: task
29 | });
30 | task.events &&
31 | task.events[event.type] &&
32 | task.events[event.type](event, task);
33 | },
34 | [task]
35 | );
36 |
37 | return useMemo(() => {
38 | const points = `0,0 ${task.width},0 ${task.width},${task.height} 0,${task.height}`;
39 |
40 | const clipPathId = `gantt-elastic__project-clip-path-${task.id}`;
41 |
42 | const displayExpander =
43 | options.chart.expander.display ||
44 | (options.chart.expander.displayIfTaskListHidden &&
45 | !options.taskList.display);
46 |
47 | return (
48 |
56 | {displayExpander && (
57 |
73 |
83 |
84 | )}
85 |
130 | {options.chart.text.display && }
131 |
132 | );
133 | }, [
134 | onEmitEvent,
135 | options.chart.expander.display,
136 | options.chart.expander.displayIfTaskListHidden,
137 | options.chart.expander.offset,
138 | options.chart.expander.size,
139 | options.chart.expander.type,
140 | options.chart.text.display,
141 | options.row.height,
142 | options.taskList.display,
143 | style,
144 | task
145 | ]);
146 | };
147 |
148 | export default ChartTask;
149 |
--------------------------------------------------------------------------------
/src/components/Chart/Text.tsx:
--------------------------------------------------------------------------------
1 | import { Task } from "@/components/interfaces";
2 | import GanttElasticContext from "@/GanttElasticContext";
3 | import React, { useContext, useMemo } from "react";
4 |
5 | export interface TaskProps {
6 | task: Task;
7 | }
8 |
9 | const ChartText: React.FC = ({ task }) => {
10 | const { style, options, ctx } = useContext(GanttElasticContext);
11 |
12 | /**
13 | * Get width
14 | *
15 | * @returns {number}
16 | */
17 | const width = useMemo(() => {
18 | if (ctx) {
19 | const textStyle = style["chart-row-text"];
20 | ctx.font = `${textStyle["fontWight"]} ${textStyle["fontSize"]} ${textStyle["fontFamily"]}`;
21 | const textWidth = ctx.measureText(task.label).width;
22 | return textWidth + options.chart.text.xPadding * 2;
23 | }
24 | }, [ctx, options.chart.text.xPadding, style, task.label]);
25 |
26 | return useMemo(() => {
27 | const height = task.height + options.chart.grid.horizontal.gap * 2;
28 |
29 | return (
30 |
58 | );
59 | }, [
60 | options.chart.grid.horizontal.gap,
61 | options.chart.text.offset,
62 | style,
63 | task.height,
64 | task.label,
65 | task.width,
66 | task.x,
67 | task.y,
68 | width
69 | ]);
70 | };
71 |
72 | export default ChartText;
73 |
--------------------------------------------------------------------------------
/src/components/Expander.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import { emitEvent } from "@/GanttElasticEvents";
3 | import _ from "lodash";
4 | import React, { useContext, useMemo } from "react";
5 | import { Task } from "./interfaces";
6 |
7 | /**
8 | * Is current expander collapsed?
9 | *
10 | * @param tasks
11 | */
12 | function collapseState(tasks: Array): boolean {
13 | if (tasks.length === 0) {
14 | return false;
15 | }
16 | let collapsed = 0;
17 | for (let i = 0, len = tasks.length; i < len; i++) {
18 | if (tasks[i].collapsed) {
19 | collapsed++;
20 | }
21 | }
22 | return collapsed === tasks.length;
23 | }
24 |
25 | /**
26 | * Get specific class prefix
27 | *
28 | * @param type
29 | * @param full
30 | */
31 | function getClassPrefix(type: string, full = true): string {
32 | return `${full ? "gantt-elastic__" : ""}${type}-expander`;
33 | }
34 |
35 | export interface ExpanderProps {
36 | type: string;
37 | options: {
38 | type: string;
39 | size: number;
40 | padding: number;
41 | margin: number;
42 | };
43 | tasks: Array;
44 | }
45 |
46 | const state = {
47 | border: 0.5,
48 | borderStyle: {
49 | strokeWidth: 0.5
50 | },
51 | lineOffset: 5
52 | };
53 |
54 | const Expander: React.FC = ({ type, tasks, options }) => {
55 | const { style, dispatch } = useContext(GanttElasticContext);
56 |
57 | const { collapsed, toggle, allChildren } = useMemo(() => {
58 | const collapsed = collapseState(tasks);
59 | /**
60 | * Toggle expander
61 | */
62 | const toggle = (): void => {
63 | if (tasks.length === 0) {
64 | return;
65 | }
66 | // const collapse = !collapsed;
67 | // tasks.forEach(task => {
68 | // task.collapsed = collapse;
69 | // });
70 | // dispatch({
71 | // type: "taskList-collapsed-change",
72 | // payload: {}
73 | // });
74 | emitEvent.emit("taskList-collapsed-change", tasks, !collapsed);
75 | };
76 | /**
77 | * Get all tasks
78 | */
79 | const allChildren: Array = [];
80 | _.forEach(tasks, task => {
81 | _.forEach(task.allChildren, childId => {
82 | allChildren.push(childId);
83 | });
84 | });
85 |
86 | return { collapsed, toggle, allChildren };
87 | }, [tasks]);
88 |
89 | return useMemo(() => {
90 | const fullClassPrefix = getClassPrefix(options.type);
91 | const notFullClassPrefix = getClassPrefix(options.type, false);
92 |
93 | const taskListStyle =
94 | type !== "taskList"
95 | ? {}
96 | : {
97 | paddingLeft:
98 | tasks[0]?.parents.length * options.padding +
99 | options.margin +
100 | "px",
101 | margin: "auto 0"
102 | };
103 | return (
104 |
111 | {allChildren.length > 0 && (
112 |
151 | )}
152 |
153 | );
154 | }, [
155 | allChildren.length,
156 | collapsed,
157 | options.margin,
158 | options.padding,
159 | options.size,
160 | options.type,
161 | style,
162 | tasks,
163 | toggle,
164 | type
165 | ]);
166 | };
167 |
168 | export default Expander;
169 |
--------------------------------------------------------------------------------
/src/components/MainView.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import { emitEvent } from "@/GanttElasticEvents";
3 | import React, {
4 | useCallback,
5 | useContext,
6 | useEffect,
7 | useMemo,
8 | useRef,
9 | useState
10 | } from "react";
11 | import Chart from "./Chart/Chart";
12 | import TaskList from "./TaskList/TaskList";
13 |
14 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
15 | export interface MainViewProps {}
16 |
17 | interface MousePos {
18 | x: number;
19 | y: number;
20 | movementX: number;
21 | movementY: number;
22 | lastX: number;
23 | lastY: number;
24 | positiveX: number;
25 | positiveY: number;
26 | currentX: number;
27 | currentY: number;
28 | }
29 |
30 | const MainView: React.FC = () => {
31 | // gantt context
32 | const {
33 | style,
34 | refs,
35 | options,
36 | chartWidth,
37 | clientHeight,
38 | clientWidth,
39 | rowsHeight,
40 | scrollBarHeight,
41 | allVisibleTasksHeight,
42 | taskList,
43 | calendar
44 | } = useContext(GanttElasticContext);
45 |
46 | const [scroll] = useState({ scrolling: false });
47 |
48 | // Chart拖动事件数据,不需要重新渲染页面
49 | const [mousePos] = useState({
50 | x: 0,
51 | y: 0,
52 | movementX: 0,
53 | movementY: 0,
54 | lastX: 0,
55 | lastY: 0,
56 | positiveX: 0,
57 | positiveY: 0,
58 | currentX: 0,
59 | currentY: 0
60 | });
61 |
62 | /**
63 | * refs
64 | */
65 | const mainViewRef = useRef(null);
66 | const taskListRef = useRef(null);
67 | const chartContainerRef = useRef(null);
68 | const chartScrollContainerVerticalRef = useRef(null);
69 | const chartScrollContainerHorizontalRef = useRef(null);
70 |
71 | useEffect(() => {
72 | refs.mainView = mainViewRef;
73 | refs.taskList = taskListRef;
74 | refs.chartContainer = chartContainerRef;
75 | refs.chartScrollContainerVertical = chartScrollContainerVerticalRef;
76 | refs.chartScrollContainerHorizontal = chartScrollContainerHorizontalRef;
77 | }, [refs]);
78 |
79 | const {
80 | mouseMove,
81 | mouseUp,
82 | onHorizontalScroll,
83 | onVerticalScroll,
84 | chartWheel
85 | } = useMemo(() => {
86 | /**
87 | * Emit event when mouse is moving inside main view
88 | */
89 | const mouseMove = (
90 | event: React.MouseEvent
91 | ): void => {
92 | emitEvent.emit("main-view-mousemove", event);
93 | };
94 |
95 | /**
96 | * Emit mouseup event inside main view
97 | */
98 | const mouseUp = (
99 | event: React.MouseEvent
100 | ): void => {
101 | scroll.scrolling = false;
102 | emitEvent.emit("main-view-mouseup", event);
103 | };
104 |
105 | /**
106 | * Horizontal scroll event handler
107 | */
108 | const onHorizontalScroll = (event: React.UIEvent): void => {
109 | emitEvent.emit("chart-scroll-horizontal", event);
110 | };
111 |
112 | /**
113 | * Vertical scroll event handler
114 | */
115 | const onVerticalScroll = (event: React.UIEvent): void => {
116 | emitEvent.emit("chart-scroll-vertical", event);
117 | };
118 |
119 | /**
120 | * Mouse wheel event handler
121 | */
122 | const chartWheel = (event: React.WheelEvent): void => {
123 | emitEvent.emit("chart-wheel", event);
124 | };
125 |
126 | return {
127 | mouseMove,
128 | mouseUp,
129 | onHorizontalScroll,
130 | onVerticalScroll,
131 | chartWheel
132 | };
133 | }, [scroll]);
134 |
135 | /**
136 | * Chart mousedown event handler
137 | * Initiates drag scrolling mode
138 | */
139 | const chartMouseDown = useCallback(
140 | ev => {
141 | if (typeof ev.touches !== "undefined") {
142 | mousePos.x = mousePos.lastX = ev.touches[0].screenX;
143 | mousePos.y = mousePos.lastY = ev.touches[0].screenY;
144 | mousePos.movementX = 0;
145 | mousePos.movementY = 0;
146 | const horizontal = chartScrollContainerHorizontalRef.current;
147 | const vertical = chartScrollContainerVerticalRef.current;
148 | if (horizontal && vertical) {
149 | mousePos.currentX = horizontal.scrollLeft;
150 | mousePos.currentY = vertical.scrollTop;
151 | }
152 | }
153 | scroll.scrolling = true;
154 | },
155 | [
156 | mousePos.currentX,
157 | mousePos.currentY,
158 | mousePos.lastX,
159 | mousePos.lastY,
160 | mousePos.movementX,
161 | mousePos.movementY,
162 | mousePos.x,
163 | mousePos.y,
164 | scroll
165 | ]
166 | );
167 |
168 | /**
169 | * Chart mouseup event handler
170 | * Deactivates drag scrolling mode
171 | */
172 | const chartMouseUp = useCallback(
173 | ev => {
174 | scroll.scrolling = false;
175 | },
176 | [scroll]
177 | );
178 |
179 | /**
180 | * Chart mousemove event handler
181 | * When in drag scrolling mode this method calculate scroll movement
182 | */
183 | const chartMouseMove = useCallback(
184 | ev => {
185 | if (scroll.scrolling) {
186 | ev.preventDefault();
187 | // ev.stopImmediatePropagation();
188 | ev.stopPropagation();
189 | const touch = typeof ev.touches !== "undefined";
190 | let movementX, movementY;
191 | if (touch) {
192 | const screenX = ev.touches[0].screenX;
193 | const screenY = ev.touches[0].screenY;
194 | movementX = mousePos.x - screenX;
195 | movementY = mousePos.y - screenY;
196 | mousePos.lastX = screenX;
197 | mousePos.lastY = screenY;
198 | } else {
199 | movementX = ev.movementX;
200 | movementY = ev.movementY;
201 | }
202 |
203 | const horizontal = chartScrollContainerHorizontalRef.current;
204 | const vertical = chartScrollContainerVerticalRef.current;
205 | if (horizontal && vertical) {
206 | let x = 0,
207 | y = 0;
208 | if (touch) {
209 | x =
210 | mousePos.currentX +
211 | movementX * options.scroll.dragXMoveMultiplier;
212 | } else {
213 | x =
214 | horizontal.scrollLeft -
215 | movementX * options.scroll.dragXMoveMultiplier;
216 | }
217 | horizontal.scrollLeft = x;
218 | if (touch) {
219 | y =
220 | mousePos.currentY +
221 | movementY * options.scroll.dragYMoveMultiplier;
222 | } else {
223 | y =
224 | vertical.scrollTop -
225 | movementY * options.scroll.dragYMoveMultiplier;
226 | }
227 | vertical.scrollTop = y;
228 | }
229 | }
230 | },
231 | [
232 | mousePos.currentX,
233 | mousePos.currentY,
234 | mousePos.lastX,
235 | mousePos.lastY,
236 | mousePos.x,
237 | mousePos.y,
238 | options.scroll.dragXMoveMultiplier,
239 | options.scroll.dragYMoveMultiplier,
240 | scroll
241 | ]
242 | );
243 |
244 | return useMemo(
245 | () => (
246 |
247 |
254 |
263 |
269 | {options.taskList.display && (
270 |
279 |
280 |
281 | )}
282 |
294 |
295 |
296 |
297 |
298 |
318 |
319 |
339 |
340 | ),
341 | [
342 | allVisibleTasksHeight,
343 | calendar.height,
344 | chartMouseDown,
345 | chartMouseMove,
346 | chartMouseUp,
347 | chartWheel,
348 | clientWidth,
349 | clientHeight,
350 | mouseMove,
351 | mouseUp,
352 | onHorizontalScroll,
353 | onVerticalScroll,
354 | options.calendar.gap,
355 | options.taskList.display,
356 | rowsHeight,
357 | scrollBarHeight,
358 | style,
359 | taskList.finalWidth,
360 | chartWidth
361 | ]
362 | );
363 | };
364 |
365 | export default MainView;
366 |
--------------------------------------------------------------------------------
/src/components/TaskList/ItemColumn.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import { emitEvent } from "@/GanttElasticEvents";
3 | import React, { useContext, useMemo } from "react";
4 | import { Task, TaskListColumnOption } from "../interfaces";
5 |
6 | export interface TaskListItemProps {
7 | task: Task;
8 | column: TaskListColumnOption;
9 | }
10 |
11 | const ItemColumn: React.FC = ({
12 | task,
13 | column,
14 | children
15 | }) => {
16 | const { style } = useContext(GanttElasticContext);
17 |
18 | return useMemo(() => {
19 | const itemColumnStyle = {
20 | ...style["task-list-item-column"],
21 | ...column.style["task-list-item-column"],
22 | width: column.finalWidth + "px",
23 | height: column.height + "px"
24 | };
25 | const wrapperStyle = {
26 | ...style["task-list-item-value-wrapper"],
27 | ...column.style["task-list-item-value-wrapper"]
28 | };
29 |
30 | const containerStyle = {
31 | ...style["task-list-item-value-container"],
32 | ...column.style["task-list-item-value-container"]
33 | };
34 | const valueStyle = {
35 | ...style["task-list-item-value"],
36 | ...column.style["task-list-item-value"]
37 | };
38 |
39 | /**
40 | * Emit event
41 | *
42 | * @param {Event} event
43 | */
44 | const onEmitEvent = (event: React.MouseEvent | React.TouchEvent): void => {
45 | const eventName = event.type;
46 | if (
47 | typeof column.events !== "undefined" &&
48 | typeof column.events[eventName] === "function"
49 | ) {
50 | column.events[eventName]({
51 | event,
52 | data: task,
53 | column: column
54 | });
55 | }
56 | emitEvent.emit(`taskList-${task.type}-${eventName}`, {
57 | event,
58 | data: task,
59 | column: column
60 | });
61 | };
62 |
63 | return (
64 |
68 |
72 | {children}
73 |
77 |
92 | {typeof column.value === "function"
93 | ? column.value(task)
94 | : task[column.value]}
95 |
96 |
97 |
98 |
99 | );
100 | }, [style, column, children, task]);
101 | };
102 |
103 | export default ItemColumn;
104 |
--------------------------------------------------------------------------------
/src/components/TaskList/TaskList.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import _ from "lodash";
3 | import React, { useContext, useEffect, useMemo, useRef } from "react";
4 | import TaskListHeader from "./TaskListHeader";
5 | import TaskListItem from "./TaskListItem";
6 |
7 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
8 | export interface TaskListProps {}
9 |
10 | const TaskList: React.FC = () => {
11 | const { style, visibleTasks, rowsHeight, refs } = useContext(
12 | GanttElasticContext
13 | );
14 |
15 | /**
16 | * refs
17 | */
18 | const taskListWrapperRef = useRef(null);
19 | const taskListRef = useRef(null);
20 | const taskListItemsRef = useRef(null);
21 |
22 | useEffect(() => {
23 | refs.taskListWrapper = taskListWrapperRef;
24 | refs.taskList = taskListRef;
25 | refs.taskListItems = taskListItemsRef;
26 | }, [refs]);
27 |
28 | return useMemo(
29 | () => (
30 |
39 |
46 |
47 |
55 | {_.map(visibleTasks, task => (
56 |
57 | ))}
58 |
59 |
60 |
61 | ),
62 | [rowsHeight, style, visibleTasks]
63 | );
64 | };
65 |
66 | export default TaskList;
67 |
--------------------------------------------------------------------------------
/src/components/TaskList/TaskListHeader.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import { emitEvent } from "@/GanttElasticEvents";
3 | import _ from "lodash";
4 | import React, {
5 | useCallback,
6 | useContext,
7 | useEffect,
8 | useMemo,
9 | useState
10 | } from "react";
11 | import Expander from "../Expander";
12 | import { TaskListColumnOption } from "../interfaces";
13 |
14 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
15 | export interface TaskListHeaderProps {}
16 |
17 | const TaskListHeader: React.FC = () => {
18 | const { style, taskList, allTasks, calendar, options } = useContext(
19 | GanttElasticContext
20 | );
21 |
22 | const [resizer] = useState<{
23 | moving?: TaskListColumnOption;
24 | x: number;
25 | initialWidth: number;
26 | }>({
27 | // moving: false,
28 | x: 0,
29 | initialWidth: 0
30 | });
31 |
32 | /**
33 | * Resizer mouse down event handler
34 | */
35 | const resizerMouseDown = useCallback(
36 | column => {
37 | return (event: React.MouseEvent): void => {
38 | if (!resizer.moving) {
39 | resizer.moving = column;
40 | resizer.x = event.clientX;
41 | resizer.initialWidth = column.width;
42 | emitEvent.emit("taskList-column-width-change-start", resizer.moving);
43 | }
44 | };
45 | },
46 | [resizer]
47 | );
48 |
49 | /**
50 | * Resizer mouse move event handler
51 | */
52 | const resizerMouseMove = useCallback(
53 | event => {
54 | if (resizer.moving) {
55 | const lastWidth = resizer.moving.width;
56 | resizer.moving.width = resizer.initialWidth + event.clientX - resizer.x;
57 | if (resizer.moving.width < options.taskList.minWidth) {
58 | resizer.moving.width = options.taskList.minWidth;
59 | }
60 | if (lastWidth !== resizer.moving.width) {
61 | // set({ ...resizer });
62 | // dispatch({
63 | // payload: {
64 | // options: {
65 | // ...options,
66 | // taskList: { ...taskList, columns: [...taskList.columns] }
67 | // }
68 | // }
69 | // });
70 | emitEvent.emit("taskList-column-width-change", resizer.moving);
71 | }
72 | }
73 | },
74 | [resizer.initialWidth, resizer.moving, resizer.x, options.taskList.minWidth]
75 | );
76 |
77 | /**
78 | * Resizer mouse up event handler
79 | */
80 | const resizerMouseUp = useCallback((): void => {
81 | if (resizer.moving) {
82 | resizer.moving = undefined;
83 | emitEvent.emit("taskList-column-width-change-stop", resizer.moving);
84 | }
85 | }, [resizer]);
86 |
87 | useEffect(() => {
88 | document.addEventListener("mouseup", resizerMouseUp);
89 | document.addEventListener("mousemove", resizerMouseMove);
90 | emitEvent.on("main-view-mousemove", resizerMouseMove);
91 | emitEvent.on("main-view-mouseup", resizerMouseUp);
92 | return (): void => {
93 | document.removeEventListener("mouseup", resizerMouseUp);
94 | document.removeEventListener("mousemove", resizerMouseMove);
95 | emitEvent.removeAllListeners("main-view-mousemove");
96 | emitEvent.removeAllListeners("main-view-mouseup");
97 | };
98 | }, [resizerMouseMove, resizerMouseUp]);
99 |
100 | return useMemo(
101 | () => (
102 |
110 | {_.map(taskList.columns, column => {
111 | return (
112 |
121 | {column.expander && (
122 |
task.allChildren.length > 0
127 | )}
128 | options={options.taskList.expander}
129 | >
130 | )}
131 |
139 | {column.label}
140 |
141 |
179 |
180 | );
181 | })}
182 |
183 | ),
184 | [
185 | allTasks,
186 | calendar.height,
187 | options.calendar.gap,
188 | options.taskList.expander,
189 | resizerMouseDown,
190 | resizerMouseUp,
191 | style,
192 | taskList.columns
193 | ]
194 | );
195 | };
196 |
197 | export default TaskListHeader;
198 |
--------------------------------------------------------------------------------
/src/components/TaskList/TaskListItem.tsx:
--------------------------------------------------------------------------------
1 | import GanttElasticContext from "@/GanttElasticContext";
2 | import _ from "lodash";
3 | import React, { useContext, useMemo } from "react";
4 | import Expander from "../Expander";
5 | import { Task } from "../interfaces";
6 | import ItemColumn from "./ItemColumn";
7 |
8 | export interface TaskListItemProps {
9 | task: Task;
10 | }
11 |
12 | const TaskListItem: React.FC = ({ task }) => {
13 | const { style, options, taskList } = useContext(GanttElasticContext);
14 |
15 | return useMemo(() => {
16 | const tasks = [task];
17 | return (
18 |
24 | {_.map(taskList.columns, (column, index) => (
25 |
30 | {column.expander && (
31 |
36 | )}
37 |
38 | ))}
39 |
40 | );
41 | }, [options.taskList.expander, style, task, taskList.columns]);
42 | };
43 |
44 | export default TaskListItem;
45 |
--------------------------------------------------------------------------------
/src/components/interfaces.d.ts:
--------------------------------------------------------------------------------
1 | import { Task } from "@/types";
2 |
3 | export interface CalendarRowItems {
4 | key: string;
5 | children: Array;
6 | }
7 |
8 | export interface CalendarRowText {
9 | index: number;
10 | key: string;
11 | x: number;
12 | y: number;
13 | width: number;
14 | textWidth: number;
15 | choosenFormatName?: string;
16 | height: number;
17 | label: string;
18 | }
19 |
20 | export interface TaskMap {
21 | [key: string]: Task;
22 | }
23 |
24 | export interface RootTask {
25 | id: null;
26 | label: "root";
27 | children: [];
28 | allChildren: [];
29 | parents: [];
30 | parent: null;
31 | dependentOn: [];
32 | parentId: null;
33 | __root: boolean;
34 | }
35 |
36 | export interface Task {
37 | id: React.ReactText; // *
38 | dependencyLines: Array<{ points?: string; taskId: React.ReactText }>;
39 | dependentOn: Array;
40 | parentId: React.ReactText | null;
41 | start: dayjs.ConfigType; // *
42 | startTime: number;
43 | end: dayjs.ConfigType;
44 | endTime: number;
45 | label: string; // *
46 | duration: number; // *
47 | progress: number; // *
48 | type: string;
49 | style: DynamicStyle;
50 | collapsed: boolean;
51 | // extends
52 | // parent: Task | null | string | number;
53 | // parents: Task[] | string[] | number[];
54 | // allChildren: Task[] | string[] | number[];
55 | // children: Task[] | string[] | number[];
56 | parent: React.ReactText | null;
57 | parents: Array;
58 | allChildren: Array;
59 | children: Array;
60 | mouseOver: boolean;
61 | height: number;
62 | width: number;
63 | y: number;
64 | x: number;
65 | __root?: boolean;
66 | events?: {
67 | [key: string]: (
68 | event: React.MouseEvent | React.TouchEvent,
69 | task: Task
70 | ) => void;
71 | };
72 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
73 | [key: string]: any;
74 | }
75 |
76 | export interface GanttElasticRefs {
77 | taskListWrapper?: React.RefObject;
78 | chartGraphSvg?: React.RefObject;
79 | taskList?: React.RefObject;
80 | svgChart?: React.RefObject;
81 | taskListItems?: React.RefObject;
82 | chartGraph?: React.RefObject;
83 | chartCalendarContainer?: React.RefObject;
84 | chartGraphContainer?: React.RefObject;
85 | chartScrollContainerVertical?: React.RefObject;
86 | chartScrollContainerHorizontal?: React.RefObject;
87 | mainView?: React.RefObject;
88 | chartContainer?: React.RefObject;
89 | chart?: React.RefObject;
90 | }
91 |
92 | export interface ScrollOption {
93 | scrolling: boolean;
94 | top: number;
95 | left: number;
96 | taskList: {
97 | left: number;
98 | right: number;
99 | top: number;
100 | bottom: number;
101 | };
102 | chart: {
103 | left: number;
104 | right: number;
105 | percent: number;
106 | timePercent: number;
107 | top: number;
108 | bottom: number;
109 | time: number;
110 | timeCenter: number;
111 | dateTime: {
112 | left: number;
113 | right: number;
114 | };
115 | };
116 | }
117 |
118 | export interface TimeOption {
119 | firstTime: number;
120 | lastTime: number;
121 | totalViewDurationMs: number;
122 | totalViewDurationPx: number;
123 | steps: StepOption[];
124 | }
125 |
126 | export interface StepOption {
127 | time: number;
128 | offset: { ms: number; px: number };
129 | width: { ms: number; px: number };
130 | }
131 | export interface TaskListOption {
132 | columns: TaskListColumnOption[];
133 | width: number;
134 | finalWidth: number;
135 | widthFromPercentage: number;
136 | }
137 |
138 | export interface TaskListColumnOption {
139 | id: number;
140 | label: string;
141 | value: string | Function;
142 | width: number;
143 | height: number;
144 | style: DynamicStyle;
145 | finalWidth: number;
146 | thresholdPercent: number;
147 | widthFromPercentage: number;
148 | events?: {
149 | [key: string]: (a: {
150 | event: React.MouseEvent | React.TouchEvent;
151 | data: Task;
152 | column: TaskListColumnOption;
153 | }) => void;
154 | };
155 | expander: boolean;
156 | _key: string;
157 | }
158 |
159 | export interface CalendarItemWidths {
160 | short?: number;
161 | medium?: number;
162 | long?: number;
163 | [key: string]: number;
164 | }
165 |
166 | export interface Formatted {
167 | long: string[];
168 | medium: string[];
169 | short: string[];
170 | [key: string]: string[];
171 | }
172 |
173 | export interface CalendarOption {
174 | height: number;
175 | hour: {
176 | widths: CalendarItemWidths[];
177 | maxWidths: CalendarItemWidths;
178 | formatted: Formatted;
179 | };
180 | day: {
181 | widths: CalendarItemWidths[];
182 | maxWidths: CalendarItemWidths;
183 | };
184 | month: {
185 | widths: CalendarItemWidths[];
186 | maxWidths: CalendarItemWidths;
187 | };
188 | }
189 | export interface State {
190 | // width: number;
191 | // height: number;
192 | // clientWidth: number;
193 | // outerHeight: number;
194 | // rowsHeight: number;
195 | // scrollBarHeight: number;
196 | // allVisibleTasksHeight: number;
197 | scroll: ScrollOption;
198 | times: TimeOption;
199 | taskList: TaskListOption;
200 | calendar: CalendarOption;
201 | }
202 |
203 | export interface Options {
204 | scroll: {
205 | dragXMoveMultiplier: number; //*
206 | dragYMoveMultiplier: number; //*
207 | };
208 | scope: {
209 | //*
210 | before: number;
211 | after: number;
212 | };
213 | times: {
214 | timeScale: number;
215 | timeZoom: number; //*
216 | firstTime: number;
217 | lastTime: number;
218 | timePerPixel: number; // 计算值,设置无效
219 | stepDuration: dayjs.UnitType;
220 | };
221 | row: {
222 | height: number; //*
223 | };
224 | maxRows: number; //*
225 | maxHeight: number; //*
226 | chart: {
227 | grid: {
228 | horizontal: {
229 | gap: number; //*
230 | };
231 | };
232 | progress: {
233 | width: number; //*
234 | height: number; //*
235 | pattern: boolean;
236 | bar: boolean;
237 | };
238 | text: {
239 | offset: number; //*
240 | xPadding: number; //*
241 | display: boolean; //*
242 | };
243 | expander: {
244 | type: string;
245 | display: boolean; //*
246 | displayIfTaskListHidden: boolean; //*
247 | offset: number; //*
248 | size: number;
249 | };
250 | };
251 | taskList: {
252 | display: boolean; //*
253 | resizeAfterThreshold: boolean; //*
254 | widthThreshold: number; //*
255 | columns: GanttElasticTaskListColumn[];
256 | percent: number; //*
257 | minWidth: number;
258 | expander: {
259 | type: string;
260 | size: number;
261 | columnWidth: number;
262 | padding: number;
263 | margin: number;
264 | straight: boolean;
265 | };
266 | };
267 | calendar: {
268 | workingDays: number[]; //*
269 | gap: number; //*
270 | strokeWidth: number;
271 | hour: {
272 | height: number; //*
273 | display: boolean; //*
274 | format: DateFormat; //*
275 | };
276 | day: {
277 | height: number; //*
278 | display: boolean; //*
279 | format: DateFormat; //*
280 | };
281 | month: {
282 | height: number; //*
283 | display: boolean; //*
284 | format: DateFormat; //*
285 | };
286 | };
287 | locale: ILocale;
288 | }
289 |
--------------------------------------------------------------------------------
/src/components/utils/charts.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import _ from "lodash";
3 | import { StepOption } from "../interfaces";
4 |
5 | /**
6 | * Determine if element is inside current view port
7 | *
8 | * @param left scroll.chart.left
9 | * @param right scroll.chart.right
10 | * @param x step.offset.px
11 | * @param width
12 | * @param buffer
13 | */
14 | const isInsideViewPort = (
15 | left: number,
16 | right: number,
17 | x: number,
18 | width: number,
19 | buffer = 5000
20 | ): boolean => {
21 | return (
22 | (x + width + buffer >= left && x - buffer <= right) ||
23 | (x - buffer <= left && x + width + buffer >= right)
24 | );
25 | };
26 |
27 | /**
28 | * Get working days
29 | *
30 | * @param steps options.times.steps
31 | * @param workingDays options.calendar.workingDays
32 | */
33 | const workingDays = (
34 | steps: StepOption[],
35 | workingDays: number[]
36 | ): StepOption[] => {
37 | return _.filter(steps, step => {
38 | return !workingDays.includes(dayjs(step.time).day());
39 | });
40 | };
41 |
42 | /**
43 | * Convert time (in milliseconds) to pixel offset inside chart
44 | *
45 | * @param ms
46 | * @param firstTime options.times.firstTime
47 | * @param timePerPixel options.times.timePerPixel
48 | */
49 | const timeToPixelOffsetX = (
50 | ms: number,
51 | firstTime: number,
52 | timePerPixel: number
53 | ): number => {
54 | let x = ms - firstTime;
55 | if (x) {
56 | x = x / timePerPixel;
57 | }
58 | return x;
59 | };
60 |
61 | export { isInsideViewPort, workingDays, timeToPixelOffsetX };
62 |
--------------------------------------------------------------------------------
/src/components/utils/columns.ts:
--------------------------------------------------------------------------------
1 | import { GanttElasticTaskListColumn } from "@/types";
2 | import _ from "lodash";
3 | import { Task, TaskListColumnOption } from "../interfaces";
4 | import { getMaximalExpanderWidth, getTaskHeight } from "./tasks";
5 |
6 | /**
7 | * Initialize columns
8 | *
9 | */
10 | const initialzeColumns = (
11 | columns: GanttElasticTaskListColumn[]
12 | ): TaskListColumnOption[] => {
13 | return _.map(columns, (column, index) => {
14 | return {
15 | ...column,
16 | height: 0,
17 | finalWidth: 0,
18 | thresholdPercent: 100,
19 | widthFromPercentage: 0,
20 | expander: !!column.expander,
21 | style: column.style ?? {},
22 | _key: `${index}-${column.label}`
23 | };
24 | });
25 | };
26 |
27 | /**
28 | * Calculate task list columns dimensions
29 | *
30 | * @param columns
31 | * @param tasks
32 | * @param percent options.taskList.percent
33 | * @param padding options.taskList.expander.padding
34 | * @param margin options.taskList.expander.margin
35 | * @param height options.row.height
36 | * @param gap options.chart.grid.horizontal.gap
37 | * @param strokeWidth style["grid-line-horizontal"]["strokeWidth"]
38 | */
39 | const calculateTaskListColumnsDimensions = (
40 | columns: TaskListColumnOption[],
41 | tasks: Task[],
42 | percent: number,
43 | padding: number,
44 | margin: number,
45 | height: number,
46 | gap: number,
47 | strokeWidth: number
48 | ): {
49 | columns: TaskListColumnOption[];
50 | widthFromPercentage: number;
51 | finalWidth: number;
52 | width: number;
53 | } => {
54 | let final = 0;
55 | let percentage = 0;
56 | let totalWidth = 0;
57 | for (const column of columns) {
58 | if (column.expander) {
59 | column.widthFromPercentage =
60 | ((getMaximalExpanderWidth(tasks, padding, margin) + column.width) /
61 | 100) *
62 | percent;
63 | } else {
64 | column.widthFromPercentage = (column.width / 100) * percent;
65 | }
66 | percentage += column.widthFromPercentage;
67 | column.finalWidth =
68 | (column.thresholdPercent * column.widthFromPercentage) / 100;
69 | final += column.finalWidth;
70 | column.height = getTaskHeight(height, gap, strokeWidth) - strokeWidth;
71 | totalWidth += column.width;
72 | }
73 |
74 | return {
75 | columns,
76 | widthFromPercentage: percentage,
77 | finalWidth: final,
78 | width: totalWidth
79 | };
80 | };
81 |
82 | export { calculateTaskListColumnsDimensions, initialzeColumns };
83 |
--------------------------------------------------------------------------------
/src/components/utils/options.ts:
--------------------------------------------------------------------------------
1 | import { GanttElasticOptions } from "@/types";
2 | import dayjs from "dayjs";
3 | import locale from "dayjs/locale/zh-cn";
4 | import { Options, State } from "../interfaces";
5 |
6 | const defaultOptions: Options = {
7 | // width: 0,
8 | // height: 0,
9 | // clientWidth: 0,
10 | // outerHeight: 0,
11 | // rowsHeight: 0,
12 | // scrollBarHeight: 0,
13 | // allVisibleTasksHeight: 0,
14 | scroll: {
15 | dragXMoveMultiplier: 3, //*
16 | dragYMoveMultiplier: 2 //*
17 | },
18 | scope: {
19 | //*
20 | before: 1,
21 | after: 1
22 | },
23 | times: {
24 | firstTime: 0,
25 | lastTime: 0,
26 | timeScale: 60 * 1000,
27 | timeZoom: 17, //*
28 | timePerPixel: 0,
29 | stepDuration: "day"
30 | },
31 | row: {
32 | height: 24 //*
33 | },
34 | maxRows: 20, //*
35 | maxHeight: 0, //*
36 | chart: {
37 | grid: {
38 | horizontal: {
39 | gap: 6 //*
40 | }
41 | },
42 | progress: {
43 | width: 20, //*
44 | height: 6, //*
45 | pattern: true,
46 | bar: false
47 | },
48 | text: {
49 | offset: 4, //*
50 | xPadding: 10, //*
51 | display: true //*
52 | },
53 | expander: {
54 | type: "chart",
55 | display: false, //*
56 | displayIfTaskListHidden: true, //*
57 | offset: 4, //*
58 | size: 18
59 | }
60 | },
61 | taskList: {
62 | display: true, //*
63 | resizeAfterThreshold: true, //*
64 | widthThreshold: 75, //*
65 | columns: [
66 | {
67 | id: 0,
68 | label: "ID",
69 | value: "value",
70 | width: 40,
71 | style: {}
72 | },
73 | {
74 | id: 1,
75 | label: "Task Name",
76 | value: "value",
77 | width: 200,
78 | style: {}
79 | }
80 | ],
81 | percent: 100, //*
82 | minWidth: 18,
83 | expander: {
84 | type: "task-list",
85 | size: 16,
86 | columnWidth: 24,
87 | padding: 16,
88 | margin: 10,
89 | straight: false
90 | }
91 | },
92 | calendar: {
93 | workingDays: [1, 2, 3, 4, 5], //*
94 | gap: 6, //*
95 | strokeWidth: 1,
96 | hour: {
97 | height: 20, //*
98 | display: true, //*
99 | format: {
100 | //*
101 | long(date: dayjs.Dayjs): string {
102 | return date.format("HH:mm");
103 | },
104 | medium(date: dayjs.Dayjs): string {
105 | return date.format("HH:mm");
106 | },
107 | short(date: dayjs.Dayjs): string {
108 | return date.format("HH");
109 | }
110 | }
111 | },
112 | day: {
113 | height: 20, //*
114 | display: true, //*
115 | format: {
116 | long(date: dayjs.Dayjs): string {
117 | return date.format("DD dddd");
118 | },
119 | medium(date: dayjs.Dayjs): string {
120 | return date.format("DD ddd");
121 | },
122 | short(date: dayjs.Dayjs): string {
123 | return date.format("DD");
124 | }
125 | }
126 | },
127 | month: {
128 | height: 20, //*
129 | display: true, //*
130 | format: {
131 | //*
132 | short(date: dayjs.Dayjs): string {
133 | return date.format("MM");
134 | },
135 | medium(date: dayjs.Dayjs): string {
136 | return date.format("MMM 'YY");
137 | },
138 | long(date: dayjs.Dayjs): string {
139 | return date.format("MMMM YYYY");
140 | }
141 | }
142 | }
143 | },
144 | locale
145 | };
146 |
147 | /**
148 | * Helper function to fill out empty options in user settings
149 | *
150 | * @param {object} userOptions - initial user options that will merge with those below
151 | * @returns {object} merged options
152 | */
153 | const getOptions = (userOptions?: Partial): Options => {
154 | let localeName = "en";
155 | if (
156 | userOptions &&
157 | typeof userOptions.locale !== "undefined" &&
158 | typeof userOptions.locale.name !== "undefined"
159 | ) {
160 | localeName = userOptions.locale.name;
161 | }
162 | return {
163 | ...defaultOptions
164 | };
165 | };
166 |
167 | const defaultState: State = {
168 | width: 0,
169 | // height: 0,
170 | // clientWidth: 0,
171 | // outerHeight: 0,
172 | // rowsHeight: 0,
173 | // scrollBarHeight: 0,
174 | // allVisibleTasksHeight: 0,
175 |
176 | scroll: {
177 | scrolling: false,
178 | top: 0,
179 | left: 0,
180 | taskList: {
181 | left: 0,
182 | right: 0,
183 | top: 0,
184 | bottom: 0
185 | },
186 | chart: {
187 | left: 0,
188 | right: 0,
189 | percent: 0,
190 | timePercent: 0,
191 | top: 0,
192 | bottom: 0,
193 | time: 0,
194 | timeCenter: 0,
195 | dateTime: {
196 | left: 0,
197 | right: 0
198 | }
199 | }
200 | },
201 | times: {
202 | firstTime: 0,
203 | lastTime: 0,
204 | totalViewDurationMs: 0,
205 | totalViewDurationPx: 0,
206 | steps: []
207 | },
208 | taskList: {
209 | columns: [],
210 | width: 0,
211 | finalWidth: 0,
212 | widthFromPercentage: 0
213 | },
214 | calendar: {
215 | height: 0,
216 | hour: {
217 | widths: [],
218 | maxWidths: { short: 0, medium: 0, long: 0 },
219 | formatted: {
220 | long: [],
221 | medium: [],
222 | short: []
223 | }
224 | },
225 | day: {
226 | widths: [],
227 | maxWidths: { short: 0, medium: 0, long: 0 }
228 | },
229 | month: {
230 | widths: [],
231 | maxWidths: { short: 0, medium: 0, long: 0 }
232 | }
233 | }
234 | };
235 |
236 | export { getOptions, defaultState, defaultOptions };
237 |
--------------------------------------------------------------------------------
/src/components/utils/style.ts:
--------------------------------------------------------------------------------
1 | import { DynamicStyle } from "@/types";
2 |
3 | const getStyle = (
4 | fontSize = "12px",
5 | fontFamily = "Arial, sans-serif"
6 | ): DynamicStyle => {
7 | return {
8 | "main-view": {
9 | background: "#FFFFFF"
10 | },
11 | "main-container-wrapper": {
12 | overflow: "hidden",
13 | borderTop: "1px solid #eee",
14 | borderBottom: "1px solid #eee"
15 | },
16 | "main-container": {
17 | float: "left",
18 | maxWidth: "100%"
19 | },
20 | "main-view-container": {},
21 | container: {
22 | display: "flex",
23 | maxWidth: "100%",
24 | height: "100%"
25 | },
26 | "calendar-wrapper": {
27 | userSelect: "none"
28 | },
29 | calendar: {
30 | width: "100%",
31 | background: "#f3f5f7",
32 | display: "block"
33 | },
34 | "calendar-row": {
35 | display: "flex",
36 | justifyContent: "space-evenly"
37 | },
38 | "calendar-row--month": {},
39 | "calendar-row--day": {},
40 | "calendar-row--hour": {
41 | borderBottom: "1px solid #eee"
42 | },
43 | "calendar-row-rect": {
44 | background: "transparent",
45 | display: "flex"
46 | },
47 | "calendar-row-rect--month": {},
48 | "calendar-row-rect--day": {},
49 | "calendar-row-rect--hour": {},
50 | "calendar-row-rect-child": {
51 | display: "block",
52 | borderRightWidth: "1px", // Calendar
53 | borderRightColor: "#dadada",
54 | borderRightStyle: "solid",
55 | position: "relative"
56 | },
57 | "calendar-row-rect-child--month": {},
58 | "calendar-row-rect-child--day": { textAlign: "center" },
59 | "calendar-row-rect-child--hour": { textAlign: "center" },
60 | "calendar-row-text": {
61 | fontFamily, // GanttElastic
62 | fontSize, //GanttElastic
63 | color: "#606060",
64 | display: "inline-block",
65 | position: "relative"
66 | },
67 | "calendar-row-text--month": {},
68 | "calendar-row-text--day": {},
69 | "calendar-row-text--hour": {},
70 | "task-list-wrapper": {},
71 | "task-list": { background: "transparent", borderColor: "#eee" },
72 | "task-list-header": {
73 | display: "flex",
74 | userSelect: "none",
75 | verticalAlign: "middle",
76 | borderBottom: "1px solid #eee",
77 | borderLeft: "1px solid #eee"
78 | },
79 | "task-list-header-column": {
80 | borderLeft: "1px solid #00000050",
81 | boxSizing: "border-box",
82 | display: "flex",
83 | background: "#f3f5f7",
84 | borderColor: "transparent"
85 | },
86 | "task-list-expander-wrapper": {
87 | display: "inline-flex",
88 | flexShrink: 0,
89 | boxSizing: "border-box",
90 | margin: "0 0 0 10px"
91 | },
92 | "task-list-expander-content": {
93 | display: "inline-flex",
94 | cursor: "pointer",
95 | margin: "auto 0px",
96 | boxSizing: "border-box",
97 | userSelect: "none"
98 | },
99 | "task-list-expander-line": {
100 | fill: "transparent",
101 | stroke: "#000000",
102 | strokeWidth: 1,
103 | strokeLinecap: "round"
104 | },
105 | "task-list-expander-border": {
106 | fill: "#ffffffa0",
107 | stroke: "#000000A0"
108 | },
109 | "chart-expander-wrapper": {
110 | display: "block",
111 | lineHeight: 1,
112 | boxSizing: "border-box",
113 | margin: "0"
114 | },
115 | "chart-expander-content": {
116 | display: "inline-flex",
117 | cursor: "pointer",
118 | margin: "auto 0px",
119 | boxSizing: "border-box",
120 | userSelect: "none"
121 | },
122 | "chart-expander-line": {
123 | fill: "transparent",
124 | stroke: "#000000",
125 | strokeWidth: 1,
126 | strokeLinecap: "round"
127 | },
128 | "chart-expander-border": {
129 | fill: "#ffffffa0",
130 | stroke: "#000000A0"
131 | },
132 | "task-list-container": {},
133 | "task-list-header-label": {
134 | overflow: "hidden",
135 | textOverflow: "ellipsis",
136 | fontFamily,
137 | fontSize,
138 | boxSizing: "border-box",
139 | margin: "auto 6px",
140 | flexGrow: 1,
141 | verticalAlign: "middle"
142 | },
143 | "task-list-header-resizer-wrapper": {
144 | background: "transparent",
145 | height: "100%",
146 | width: "6px",
147 | cursor: "col-resize",
148 | display: "inline-flex",
149 | verticalAlign: "center"
150 | },
151 | "task-list-header-resizer": { margin: "auto 0px" },
152 | "task-list-header-resizer-dot": {
153 | width: "3px",
154 | height: "3px",
155 | background: "#ddd",
156 | borderRadius: "100%",
157 | margin: "4px 0px"
158 | },
159 | "task-list-items": {
160 | overflow: "hidden"
161 | },
162 | "task-list-item": {
163 | borderTop: "1px solid #eee",
164 | borderRight: "1px solid #eee",
165 | boxSizing: "border-box",
166 | display: "flex",
167 | background: "transparent"
168 | },
169 | "task-list-item-column": {
170 | display: "inline-flex",
171 | flexShrink: 0,
172 | borderLeft: "1px solid #00000050",
173 | boxSizing: "border-box",
174 | borderColor: "#eee"
175 | },
176 | "task-list-item-value-wrapper": {
177 | overflow: "hidden",
178 | display: "flex",
179 | width: "100%"
180 | },
181 | "task-list-item-value-container": {
182 | margin: "auto 0px",
183 | overflow: "hidden"
184 | },
185 | "task-list-item-value": {
186 | display: "block",
187 | flexShrink: 100,
188 | fontFamily,
189 | fontSize,
190 | marginTop: "auto",
191 | marginBottom: "auto",
192 | marginLeft: "6px", // TaskList
193 | marginRight: "6px",
194 | overflow: "hidden",
195 | textOverflow: "ellipsis",
196 | lineHeight: "1.5em",
197 | wordBreak: "keep-all",
198 | whiteSpace: "nowrap",
199 | color: "#606060",
200 | background: "#FFFFFF"
201 | },
202 | "grid-lines": {},
203 | "grid-line-horizontal": {
204 | stroke: "#00000010",
205 | strokeWidth: 1
206 | },
207 | "grid-line-vertical": {
208 | stroke: "#00000010",
209 | strokeWidth: 1
210 | },
211 | "grid-line-time": {
212 | stroke: "#FF000080",
213 | strokeWidth: 1
214 | },
215 | chart: {
216 | userSelect: "none",
217 | overflow: "hidden"
218 | },
219 | "chart-calendar-container": {
220 | userSelect: "none",
221 | overflow: "hidden",
222 | maxWidth: "100%",
223 | borderRight: "1px solid #eee"
224 | },
225 | "chart-graph-container": {
226 | userSelect: "none",
227 | overflow: "hidden",
228 | maxWidth: "100%",
229 | borderRight: "1px solid #eee"
230 | },
231 | "chart-area": {},
232 | "chart-graph": {
233 | overflow: "hidden"
234 | },
235 | "chart-row-text-wrapper": {},
236 | "chart-row-text": {
237 | background: "#ffffffa0",
238 | borderRadius: "10px",
239 | fontFamily,
240 | fontSize,
241 | fontWeight: "normal",
242 | color: "#000000a0",
243 | height: "100%",
244 | display: "inline-block"
245 | },
246 | "chart-row-text-content": {
247 | padding: "0px 6px"
248 | },
249 | "chart-row-text-content--text": {},
250 | "chart-row-text-content--html": {},
251 | "chart-row-wrapper": {},
252 | "chart-row-bar-wrapper": {},
253 | "chart-row-bar": {},
254 | "chart-row-bar-polygon": {
255 | stroke: "#E74C3C",
256 | strokeWidth: 1,
257 | fill: "#F75C4C"
258 | },
259 | "chart-row-project-wrapper": {},
260 | "chart-row-project": {},
261 | "chart-row-project-polygon": {},
262 | "chart-row-milestone-wrapper": {},
263 | "chart-row-milestone": {},
264 | "chart-row-milestone-polygon": {},
265 | "chart-row-task-wrapper": {},
266 | "chart-row-task": {},
267 | "chart-row-task-polygon": {},
268 | "chart-row-progress-bar-wrapper": {},
269 | "chart-row-progress-bar": {},
270 | "chart-row-progress-bar-line": {
271 | stroke: "#ffffff25",
272 | strokeWidth: 20
273 | },
274 | "chart-row-progress-bar-solid": {
275 | fill: "#0EAC51",
276 | height: "20%"
277 | },
278 | "chart-row-progress-bar-pattern": {
279 | fill: "url(#diagonalHatch)",
280 | transform: "translateY(0.1) scaleY(0.8)"
281 | },
282 | "chart-row-progress-bar-outline": {
283 | stroke: "#E74C3C",
284 | strokeWidth: 1
285 | },
286 | "chart-dependency-lines-wrapper": {},
287 | "chart-dependency-lines-path": {
288 | fill: "transparent",
289 | stroke: "#FFa00090",
290 | strokeWidth: 2
291 | },
292 | "chart-scroll-container": {},
293 | "chart-scroll-container--horizontal": {
294 | overflow: "auto",
295 | maxWidth: "100%"
296 | },
297 | "chart-scroll-container--vertical": {
298 | overflowY: "auto",
299 | overflowX: "hidden",
300 | maxHeight: "100%",
301 | float: "right"
302 | },
303 | "chart-days-highlight-rect": {
304 | fill: "#f3f5f780"
305 | },
306 | "slot-header-beforeOptions": {
307 | display: "inline-block"
308 | }
309 | };
310 | };
311 |
312 | const prepareStyle = (userStyle?: DynamicStyle): DynamicStyle => {
313 | let fontSize: any = "12px";
314 | let fontFamily = window
315 | .getComputedStyle(document.body)
316 | .getPropertyValue("font-family")
317 | .toString();
318 | if (typeof userStyle !== "undefined") {
319 | if (typeof userStyle.fontSize !== "undefined") {
320 | fontSize = userStyle.fontSize;
321 | }
322 | if (typeof userStyle.fontFamily !== "undefined") {
323 | fontFamily = `${userStyle.fontFamily}`;
324 | }
325 | }
326 | // return getStyle(fontSize, fontFamily);
327 | return getStyle(fontSize, fontFamily);
328 | };
329 |
330 | export { getStyle, prepareStyle };
331 |
--------------------------------------------------------------------------------
/src/components/utils/tasks.ts:
--------------------------------------------------------------------------------
1 | import { GanttElasticTask } from "@/types";
2 | import dayjs from "dayjs";
3 | import _ from "lodash";
4 | import { Task } from "../interfaces";
5 |
6 | /**
7 | * Fill out empty task properties and make it reactive
8 | */
9 | const fillTasks = (inTask: Partial): Task => {
10 | const task: Task = {
11 | id: 0,
12 | type: "task",
13 | startTime: 0,
14 | endTime: 0,
15 | start: 0,
16 | label: "task",
17 | progress: 0,
18 | end: 0,
19 | duration: 0,
20 | children: [],
21 | allChildren: [],
22 | parents: [],
23 | parent: null,
24 | dependentOn: [],
25 | dependencyLines: [],
26 | parentId: null,
27 | style: {},
28 | collapsed: false,
29 | mouseOver: false,
30 | height: 0,
31 | width: 0,
32 | y: 0,
33 | x: 0,
34 | ...inTask
35 | };
36 |
37 | if (_.isEmpty(task.parentId)) {
38 | task.parentId = 0;
39 | }
40 | if (_.isEmpty(task.startTime)) {
41 | task.startTime = dayjs(task.start).valueOf();
42 | }
43 | if (_.isEmpty(task.endTime) && task.end) {
44 | task.endTime = dayjs(task.end).valueOf();
45 | } else if (_.isEmpty(task.endTime) && task.duration) {
46 | task.endTime = task.startTime + task.duration;
47 | }
48 | if (_.isEmpty(task.duration) && task.endTime) {
49 | task.duration = task.endTime - task.startTime;
50 | }
51 | return task;
52 | };
53 |
54 | /**
55 | * Make task tree, after reset - look above
56 | *
57 | * @param {object} task
58 | * @returns {object} tasks with children and parents
59 | */
60 | const makeTaskTree = (task: Task, tasks: Task[]): Task => {
61 | for (let i = 0, len = tasks.length; i < len; i++) {
62 | let current = tasks[i];
63 | // 当taskId是空时,task时rootTask,如果parentId也是空时,则应该时rootTask的子元素
64 | if (current.parentId === task.id) {
65 | if (task.parents.length) {
66 | task.parents.forEach(parent => current.parents.push(parent));
67 | }
68 | // eslint-disable-next-line no-prototype-builtins
69 | if (!task.propertyIsEnumerable("__root")) {
70 | current.parents.push(task.id);
71 | current.parent = task.id;
72 | } else {
73 | current.parents = [];
74 | current.parent = null;
75 | }
76 | current = makeTaskTree(current, tasks);
77 | task.allChildren.push(current.id);
78 | task.children.push(current.id);
79 | current.allChildren.forEach(childId => task.allChildren.push(childId));
80 | }
81 | }
82 | return task;
83 | };
84 |
85 | /**
86 | * Convert time (in milliseconds) to pixel offset inside chart
87 | *
88 | */
89 | const timeToPixelOffsetX = (
90 | ms: number,
91 | firstTime: number,
92 | timePerPixel: number
93 | ): number => {
94 | let x = ms - firstTime;
95 | if (x) {
96 | x = x / timePerPixel;
97 | }
98 | return x;
99 | };
100 |
101 | /**
102 | * recalculate task variables
103 | *
104 | * @param allTasks
105 | * @param firstTime options.times.firstTime,
106 | * @param timePerPixel options.times.timePerPixel
107 | * @param rowHeight options.row.height
108 | * @param horizontalGap options.chart.grid.horizontal.gap
109 | * @param strokeWidth
110 | */
111 | const recalculateTasks = (
112 | allTasks: Task[],
113 | firstTime: number,
114 | timePerPixel: number,
115 | rowHeight: number,
116 | horizontalGap: number,
117 | strokeWidth: number
118 | ): Task[] => {
119 | return _.map(allTasks, (task, index) => {
120 | let width = task.duration / timePerPixel - strokeWidth;
121 | if (width < 0) {
122 | width = 0;
123 | }
124 | const x = timeToPixelOffsetX(task.startTime, firstTime, timePerPixel);
125 | const y = (rowHeight + horizontalGap * 2) * index + horizontalGap;
126 | return {
127 | ...task,
128 | width,
129 | height: rowHeight,
130 | x,
131 | y
132 | };
133 | });
134 | };
135 |
136 | /**
137 | * Get maximal level of nested task children
138 | *
139 | * @param tasks
140 | */
141 | const getMaximalLevel = (tasks: Task[]): number => {
142 | let maximalLevel = 0;
143 | _.forEach(tasks, task => {
144 | if (task.parents.length > maximalLevel) {
145 | maximalLevel = task.parents.length;
146 | }
147 | });
148 | return maximalLevel - 1;
149 | };
150 | /**
151 | * Get maximal expander width - to calculate straight task list text
152 | *
153 | * @param tasks
154 | * @param padding options.taskList.expander.padding
155 | * @param margin options.taskList.expander.margin
156 | */
157 | const getMaximalExpanderWidth = (
158 | tasks: Task[],
159 | padding: number,
160 | margin: number
161 | ): number => {
162 | return getMaximalLevel(tasks) * padding + margin;
163 | };
164 |
165 | /**
166 | * Get one task height
167 | * @param height options.row.height
168 | * @param gap options.chart.grid.horizontal.gap
169 | * @param strokeWidth style["grid-line-horizontal"]["strokeWidth"]
170 | * @param withStroke
171 | */
172 | const getTaskHeight = (
173 | height: number,
174 | gap: number,
175 | strokeWidth: number,
176 | withStroke = false
177 | ): number => {
178 | if (withStroke) {
179 | return (
180 | height + gap * 2 + strokeWidth
181 | // parseInt(`${style["grid-line-horizontal"]["strokeWidth"]}`)
182 | );
183 | }
184 | return height + gap * 2;
185 | };
186 |
187 | /**
188 | * Get specified tasks height
189 | *
190 | * @param visibleTasks
191 | * @param height options.row.height
192 | * @param gap options.chart.grid.horizontal.gap
193 | * @param strokeWidth style["grid-line-horizontal"]["strokeWidth"]
194 | */
195 | const getTasksHeight = (
196 | visibleTasks: Task[],
197 | height: number,
198 | gap: number,
199 | strokeWidth: number
200 | ): number => {
201 | return visibleTasks.length * getTaskHeight(height, gap, strokeWidth);
202 | };
203 |
204 | /**
205 | * Get gantt total height
206 | * @param visibleTasks
207 | * @param rowHeight options.row.height
208 | * @param gridHorizontalGap options.chart.grid.horizontal.gap
209 | * @param calendarGap options.calendar.gap
210 | * @param calendarStrokeWidth options.calendar.strokeWidth
211 | * @param calendarHeight options.calendar.height
212 | * @param scrollBarHeight options.scrollBarHeight
213 | * @param outer
214 | */
215 | const getHeight = (
216 | visibleTasks: Task[],
217 | rowHeight: number,
218 | gridHorizontalGap: number,
219 | calendarGap: number,
220 | calendarStrokeWidth: number,
221 | calendarHeight: number,
222 | scrollBarHeight: number,
223 | outer = false
224 | ): number => {
225 | let height =
226 | visibleTasks.length * (rowHeight + gridHorizontalGap * 2) +
227 | calendarHeight +
228 | calendarStrokeWidth +
229 | calendarGap;
230 | if (outer) {
231 | height += scrollBarHeight;
232 | }
233 | return height;
234 | };
235 |
236 | export {
237 | recalculateTasks,
238 | getMaximalLevel,
239 | getHeight,
240 | getTasksHeight,
241 | getTaskHeight,
242 | getMaximalExpanderWidth,
243 | fillTasks,
244 | makeTaskTree
245 | };
246 |
--------------------------------------------------------------------------------
/src/components/utils/times.ts:
--------------------------------------------------------------------------------
1 | import { DateFormat, DynamicStyle } from "@/types";
2 | import dayjs from "dayjs";
3 | import {
4 | CalendarItemWidths,
5 | CalendarRowItems,
6 | Formatted,
7 | Options,
8 | StepOption
9 | } from "../interfaces";
10 |
11 | export interface DateCount {
12 | count: number;
13 | type: string;
14 | }
15 |
16 | /**
17 | * RecalculateTimes function's part
18 | * @param totalViewDurationPx options.times.totalViewDurationPx
19 | * @param strokeWidth style["grid-line-vertical"]["strokeWidth"]
20 | * @return options.width
21 | */
22 | const width: (totalViewDurationPx: number, strokeWidth: number) => number = (
23 | totalViewDurationPx,
24 | strokeWidth
25 | ) => {
26 | return totalViewDurationPx + strokeWidth;
27 | };
28 |
29 | /**
30 | * RecalculateTimes function's part
31 | * @param timeScale options.times.timeScale
32 | * @param timeZoom options.times.timeZoom
33 | *
34 | * @return options.times.timePerPixel
35 | */
36 | const timePerPixel: (timeScale: number, timeZoom: number) => number = (
37 | timeScale,
38 | timeZoom
39 | ) => {
40 | const max = timeScale * 60;
41 | const min = timeScale;
42 | const steps = max / min;
43 | const percent = timeZoom / 100;
44 | return timeScale * steps * percent + Math.pow(2, timeZoom);
45 | };
46 |
47 | /**
48 | * RecalculateTimes function's part
49 | * @param firstTime options.times.firstTime
50 | * @param lastTime options.times.lastTime
51 | *
52 | * @return options.times.totalViewDurationMs
53 | */
54 | const totalViewDurationMs: (
55 | firstTime: dayjs.ConfigType,
56 | lastTime: dayjs.ConfigType
57 | ) => number = (firstTime, lastTime) => {
58 | if (typeof firstTime === "number" && typeof lastTime === "number") {
59 | return lastTime - firstTime;
60 | }
61 | return dayjs(lastTime).diff(firstTime, "millisecond");
62 | };
63 |
64 | /**
65 | *
66 | * RecalculateTimes function's part
67 | * @param totalViewDurationMs options.times.totalViewDurationMs
68 | * @param timePerPixel options.times.timePerPixel
69 | *
70 | * @return options.times.totalViewDurationPx
71 | */
72 | const totalViewDurationPx: (
73 | totalViewDurationMs: number,
74 | timePerPixel: number
75 | ) => number = (totalViewDurationMs, timePerPixel) => {
76 | return totalViewDurationMs / timePerPixel;
77 | };
78 |
79 | /**
80 | * Calculate steps
81 | * Steps are days by default
82 | * Each step contain information about time offset and pixel offset of this time inside gantt chart
83 | *
84 | */
85 | const calculateSteps: (
86 | firstTime: number,
87 | lastTime: number,
88 | timePerPixel: number,
89 | totalViewDurationMs: number,
90 | totalViewDurationPx: number,
91 | stepDuration: dayjs.UnitType
92 | ) => StepOption[] = (
93 | firstTime,
94 | lastTime,
95 | timePerPixel = 0,
96 | totalViewDurationMs = 0,
97 | totalViewDurationPx = 0,
98 | stepDuration = "day"
99 | ) => {
100 | const steps: StepOption[] = [];
101 |
102 | steps.push({
103 | time: firstTime.valueOf(),
104 | offset: {
105 | ms: 0,
106 | px: 0
107 | },
108 | width: {
109 | ms: 0,
110 | px: 0
111 | }
112 | });
113 | for (
114 | let currentDate = dayjs(firstTime)
115 | .add(1, stepDuration)
116 | .startOf("day");
117 | currentDate.valueOf() <= lastTime;
118 | currentDate = currentDate.add(1, stepDuration).startOf("day")
119 | ) {
120 | const offsetMs = currentDate.diff(firstTime, "millisecond");
121 | const offsetPx = offsetMs / timePerPixel;
122 | const step = {
123 | time: currentDate.valueOf(),
124 | offset: {
125 | ms: offsetMs,
126 | px: offsetPx
127 | },
128 | width: { ms: 0, px: 0 }
129 | };
130 | const previousStep = steps[steps.length - 1];
131 | previousStep.width = {
132 | ms: offsetMs - previousStep.offset.ms,
133 | px: offsetPx - previousStep.offset.px
134 | };
135 | steps.push(step);
136 | }
137 | const lastStep = steps[steps.length - 1];
138 | lastStep.width = {
139 | ms: totalViewDurationMs - lastStep.offset.ms,
140 | px: totalViewDurationPx - lastStep.offset.px
141 | };
142 | return steps;
143 | };
144 |
145 | /**
146 | * Compute width of calendar hours column widths basing on text widths
147 | * @param hourFormats options.calendar.hour.format
148 | * @param localeName options.locale.name
149 | * @param style
150 | * @param ctx
151 | */
152 | const computeHourWidths: (
153 | hourFormats: DateFormat,
154 | localeName: string,
155 | style: DynamicStyle,
156 | ctx: CanvasRenderingContext2D | null
157 | ) => {
158 | widths: CalendarItemWidths[];
159 | maxWidths: CalendarItemWidths;
160 | formatted: Formatted;
161 | } = (hourFormats, localeName, style, ctx) => {
162 | const widths: CalendarItemWidths[] = [];
163 | const maxWidths: CalendarItemWidths = {};
164 | const formatted: Formatted = {
165 | long: [],
166 | medium: [],
167 | short: []
168 | };
169 | if (ctx) {
170 | //
171 | const baseStyle = {
172 | ...style["calendar-row-text"],
173 | ...style["calendar-row-text--hour"]
174 | };
175 | ctx.font = baseStyle["fontSize"] + " " + baseStyle["fontFamily"];
176 |
177 | let currentDate = dayjs("2020-01-01T00:00:00").locale(localeName); // any date will be good for hours
178 |
179 | // Initialize maxWidths
180 | for (const formatName in hourFormats) {
181 | maxWidths[formatName] = 0;
182 | }
183 | // 计算Hour文本显示宽度
184 | for (let hour = 0; hour < 24; hour++) {
185 | const width: CalendarItemWidths = { hour };
186 | for (const formatName in hourFormats) {
187 | const hourFormatted = hourFormats[formatName](currentDate);
188 | width[formatName] = ctx.measureText(hourFormatted).width;
189 | formatted[formatName].push(hourFormatted);
190 | // Calculate maxWidths
191 | if (width[formatName] > maxWidths[formatName]) {
192 | maxWidths[formatName] = width[formatName];
193 | }
194 | }
195 | widths.push(width);
196 | currentDate = currentDate.add(1, "hour");
197 | }
198 | }
199 | return { widths, maxWidths, formatted };
200 | };
201 |
202 | /**
203 | *
204 | * Compute calendar days column widths basing on text widths
205 | * @param steps options.times.steps
206 | * @param dayFormats options.calendar.day.format
207 | * @param localeName options.locale.name
208 | * @param style
209 | * @param ctx
210 | */
211 | const computeDayWidths: (
212 | steps: StepOption[],
213 | dayFormats: DateFormat,
214 | localeName: string,
215 | style: DynamicStyle,
216 | ctx: CanvasRenderingContext2D | null
217 | ) => {
218 | widths: CalendarItemWidths[];
219 | maxWidths: CalendarItemWidths;
220 | } = (steps, dayFormats, localeName, style, ctx) => {
221 | const widths: CalendarItemWidths[] = [];
222 | const maxWidths: CalendarItemWidths = {};
223 | if (ctx) {
224 | //
225 | const baseStyle = {
226 | ...style["calendar-row-text"],
227 | ...style["calendar-row-text--day"]
228 | };
229 | ctx.font = baseStyle["fontSize"] + " " + baseStyle["fontFamily"];
230 |
231 | let currentDate = dayjs(steps[0].time).locale(localeName);
232 |
233 | // Initialize maxWidths
234 | for (const formatName in dayFormats) {
235 | maxWidths[formatName] = 0;
236 | }
237 | // 计算Day文本显示宽度
238 | for (let day = 0, daysLen = steps.length; day < daysLen; day++) {
239 | const width: CalendarItemWidths = { day };
240 | for (const formatName in dayFormats) {
241 | width[formatName] = ctx.measureText(
242 | dayFormats[formatName](currentDate)
243 | ).width;
244 | // Calculate maxWidths
245 | if (width[formatName] > maxWidths[formatName]) {
246 | maxWidths[formatName] = width[formatName];
247 | }
248 | }
249 | widths.push(width);
250 | currentDate = currentDate.add(1, "day");
251 | }
252 | }
253 | return { widths, maxWidths };
254 | };
255 |
256 | /**
257 | * Months count
258 | *
259 | * @description Returns number of different months in specified time range
260 | *
261 | * @param fromTime - date in ms
262 | * @param toTime - date in ms
263 | *
264 | * @returns {number} different months count
265 | */
266 | const getMonthsCount = (
267 | fromTime: dayjs.ConfigType,
268 | toTime: dayjs.ConfigType
269 | ): number => {
270 | let currentMonth = dayjs(fromTime);
271 | const endMonth = dayjs(toTime);
272 | if (currentMonth.valueOf() > endMonth.valueOf()) {
273 | return 0;
274 | }
275 | let previousMonth = currentMonth.clone();
276 | let monthsCount = 1;
277 | while (currentMonth.valueOf() <= endMonth.valueOf()) {
278 | currentMonth = currentMonth.add(1, "day");
279 | if (previousMonth.month() !== currentMonth.month()) {
280 | monthsCount++;
281 | }
282 | previousMonth = currentMonth.clone();
283 | }
284 | return monthsCount;
285 | };
286 |
287 | /**
288 | * Compute month calendar columns widths basing on text widths
289 | * @param firstTime options.times.firstTime
290 | * @param lastTime options.times.lastTime
291 | * @param monthFormats options.calendar.month.format
292 | * @param localeName options.locale.name
293 | * @param style
294 | * @param ctx
295 | */
296 | const computeMonthWidths = (
297 | firstTime: dayjs.ConfigType,
298 | lastTime: dayjs.ConfigType,
299 | monthFormats: DateFormat,
300 | localeName: string,
301 | style: DynamicStyle,
302 | ctx: CanvasRenderingContext2D | null
303 | ): { widths: CalendarItemWidths[]; maxWidths: CalendarItemWidths } => {
304 | const widths: CalendarItemWidths[] = [];
305 | const maxWidths: CalendarItemWidths = {};
306 | if (ctx) {
307 | const baseStyle = {
308 | ...style["calendar-row-text"],
309 | ...style["calendar-row-text--month"]
310 | };
311 | ctx.font = baseStyle["fontSize"] + " " + baseStyle["fontFamily"];
312 |
313 | let currentDate = dayjs(firstTime).locale(localeName);
314 | const count = getMonthsCount(firstTime, lastTime);
315 |
316 | // Initialize maxWidths
317 | for (const formatName in monthFormats) {
318 | maxWidths[formatName] = 0;
319 | }
320 | // 计算Month文本显示宽度
321 | for (let month = 0; month < count; month++) {
322 | const width: CalendarItemWidths = {
323 | month
324 | };
325 | for (const formatName in monthFormats) {
326 | width[formatName] = ctx.measureText(
327 | monthFormats[formatName](currentDate)
328 | ).width;
329 | // Calculate maxWidths
330 | if (width[formatName] > maxWidths[formatName]) {
331 | maxWidths[formatName] = width[formatName];
332 | }
333 | }
334 | widths.push(width);
335 | currentDate = currentDate.add(1, "month");
336 | }
337 | }
338 | return { widths, maxWidths };
339 | };
340 |
341 | /**
342 | * How many hours will fit?
343 | *
344 | * @param dateFormats options.calendar.hour.format
345 | * @param fullCellWidth options.times.steps[dayIndex].width.px
346 | * @param maxWidths options.calendar.hour.maxWidths
347 | */
348 | const howManyHoursFit = (
349 | dateFormats: DateFormat,
350 | fullCellWidth: number,
351 | maxWidths: CalendarItemWidths
352 | ): DateCount => {
353 | const stroke = 1;
354 | const additionalSpace = stroke + 2;
355 | for (let hours = 24; hours > 1; hours = Math.ceil(hours / 2)) {
356 | for (const formatName in dateFormats) {
357 | if (
358 | (maxWidths[formatName] + additionalSpace) * hours <= fullCellWidth &&
359 | hours > 1
360 | ) {
361 | return {
362 | count: hours,
363 | type: formatName
364 | };
365 | }
366 | }
367 | }
368 | return {
369 | count: 0,
370 | type: ""
371 | };
372 | };
373 |
374 | /**
375 | * How many days will fit?
376 | *
377 | * @param dateFormats options.calendar.day.format
378 | * @param fullWidth options.width
379 | * @param maxWidths options.calendar.day.maxWidths
380 | * @param steps
381 | */
382 | const howManyDaysFit = (
383 | dateFormats: DateFormat,
384 | fullWidth: number,
385 | maxWidths: CalendarItemWidths,
386 | stepLength: number
387 | ): DateCount => {
388 | const stroke = 1;
389 | const additionalSpace = stroke + 2;
390 |
391 | for (let days = stepLength; days > 1; days = Math.ceil(days / 2)) {
392 | for (const formatName in dateFormats) {
393 | if (
394 | (maxWidths[formatName] + additionalSpace) * days <= fullWidth &&
395 | days > 1
396 | ) {
397 | return {
398 | count: days,
399 | type: formatName
400 | };
401 | }
402 | }
403 | }
404 | return {
405 | count: 0,
406 | type: ""
407 | };
408 | };
409 |
410 | /**
411 | * How many months will fit?
412 | *
413 | * @param dateFormats options.calendar.month.format
414 | * @param fullWidth options.width
415 | * @param maxWidths options.calendar.month.maxWidths
416 | * @param monthsCount
417 | */
418 | const howManyMonthsFit = (
419 | dateFormats: DateFormat,
420 | fullWidth: number,
421 | maxWidths: CalendarItemWidths,
422 | monthsCount: number
423 | ): DateCount => {
424 | const stroke = 1;
425 | const additionalSpace = stroke + 2;
426 |
427 | if (monthsCount === 1) {
428 | for (const formatName in dateFormats) {
429 | if (maxWidths[formatName] + additionalSpace <= fullWidth) {
430 | return {
431 | count: 1,
432 | type: formatName
433 | };
434 | }
435 | }
436 | }
437 | for (let months = monthsCount; months > 1; months = Math.ceil(months / 2)) {
438 | for (const formatName in dateFormats) {
439 | if (
440 | (maxWidths[formatName] + additionalSpace) * months <= fullWidth &&
441 | months > 1
442 | ) {
443 | return {
444 | count: months,
445 | type: formatName
446 | };
447 | }
448 | }
449 | }
450 | return {
451 | count: 0,
452 | type: Object.keys(dateFormats)[0]
453 | };
454 | };
455 |
456 | /**
457 | * Sum all calendar rows height and return result
458 | *
459 | * @param hours
460 | * @param days
461 | * @param months
462 | * @param options
463 | */
464 | const calculateCalendarDimensions = (
465 | hours: CalendarRowItems[],
466 | days: CalendarRowItems[],
467 | months: CalendarRowItems[],
468 | options: Options
469 | ): number => {
470 | let height = 0;
471 | if (options.calendar.hour.display && hours && hours.length > 0) {
472 | height += options.calendar.hour.height;
473 | }
474 | if (options.calendar.day.display && days && days.length > 0) {
475 | height += options.calendar.day.height;
476 | }
477 | if (options.calendar.month.display && months && months.length > 0) {
478 | height += options.calendar.month.height;
479 | }
480 | return height;
481 | };
482 |
483 | export {
484 | timePerPixel as calculateTimePerPixel,
485 | totalViewDurationMs as calculateTotalViewDurationMs,
486 | totalViewDurationPx as calculateTotalViewDurationPx,
487 | width as calculateWidth,
488 | getMonthsCount,
489 | calculateSteps,
490 | computeHourWidths,
491 | computeDayWidths,
492 | computeMonthWidths,
493 | calculateCalendarDimensions,
494 | howManyHoursFit,
495 | howManyDaysFit,
496 | howManyMonthsFit
497 | };
498 |
--------------------------------------------------------------------------------
/src/demo.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import React from "react";
3 | import ReactDOM from "react-dom";
4 | import GanttElastic from "./GanttElastic";
5 | import "./style.css";
6 |
7 | function getDate(hours: number): number {
8 | return (
9 | dayjs()
10 | .startOf("day")
11 | .valueOf() +
12 | hours * 60 * 60 * 1000
13 | );
14 | }
15 |
16 | const tasks = [
17 | {
18 | id: 1,
19 | label: "Make some noise",
20 | user:
21 | 'John Doe',
22 | start: getDate(-24 * 5),
23 | duration: 5 * 24 * 60 * 60 * 1000,
24 | progress: 85,
25 | type: "project"
26 | },
27 | {
28 | id: 2,
29 | label: "With great power comes great responsibility",
30 | user:
31 | 'Peter Parker',
32 | parentId: 1,
33 | start: getDate(-24 * 4),
34 | duration: 4 * 24 * 60 * 60 * 1000,
35 | progress: 50,
36 | type: "milestone",
37 | style: {
38 | base: {
39 | fill: "#1EBC61",
40 | stroke: "#0EAC51"
41 | }
42 | /*'tree-row-bar': {
43 | fill: '#1EBC61',
44 | stroke: '#0EAC51'
45 | },
46 | 'tree-row-bar-polygon': {
47 | stroke: '#0EAC51'
48 | }*/
49 | }
50 | },
51 | {
52 | id: 3,
53 | label: "Courage is being scared to death, but saddling up anyway.",
54 | user:
55 | 'John Wayne',
56 | parentId: 2,
57 | start: getDate(-24 * 3),
58 | duration: 2 * 24 * 60 * 60 * 1000,
59 | progress: 100,
60 | type: "task"
61 | },
62 | {
63 | id: 4,
64 | label: "Put that toy AWAY!",
65 | user:
66 | 'Clark Kent',
67 | start: getDate(-24 * 2),
68 | duration: 2 * 24 * 60 * 60 * 1000,
69 | progress: 50,
70 | type: "task",
71 | dependentOn: [3]
72 | },
73 | {
74 | id: 5,
75 | label:
76 | "One billion, gajillion, fafillion... shabadylu...mil...shabady......uh, Yen.",
77 | user:
78 | 'Austin Powers',
79 | parentId: 4,
80 | start: getDate(0),
81 | duration: 2 * 24 * 60 * 60 * 1000,
82 | progress: 10,
83 | type: "milestone",
84 | style: {
85 | base: {
86 | fill: "#0287D0",
87 | stroke: "#0077C0"
88 | }
89 | }
90 | },
91 | {
92 | id: 6,
93 | label: "Butch Mario and the Luigi Kid",
94 | user:
95 | 'Mario Bros',
96 | parentId: 5,
97 | start: getDate(24),
98 | duration: 1 * 24 * 60 * 60 * 1000,
99 | progress: 50,
100 | type: "task",
101 | style: {
102 | base: {
103 | fill: "#8E44AD",
104 | stroke: "#7E349D"
105 | }
106 | }
107 | },
108 | {
109 | id: 7,
110 | label: "Devon, the old man wanted me, it was his dying request",
111 | user:
112 | 'Knight Rider',
113 | parentId: 2,
114 | dependentOn: [6],
115 | start: getDate(24 * 2),
116 | duration: 4 * 60 * 60 * 1000,
117 | progress: 20,
118 | type: "task"
119 | },
120 | {
121 | id: 8,
122 | label: "Hey, Baby! Anybody ever tell you I have beautiful eyes?",
123 | user:
124 | 'Johhny Bravo',
125 | parentId: 7,
126 | dependentOn: [7],
127 | start: getDate(24 * 3),
128 | duration: 1 * 24 * 60 * 60 * 1000,
129 | progress: 0,
130 | type: "task"
131 | },
132 | {
133 | id: 9,
134 | label:
135 | "This better be important, woman. You are interrupting my very delicate calculations.",
136 | user:
137 | 'Dexter\'s Laboratory',
138 | parentId: 8,
139 | dependentOn: [8, 7],
140 | start: getDate(24 * 4),
141 | duration: 4 * 60 * 60 * 1000,
142 | progress: 20,
143 | type: "task",
144 | style: {
145 | base: {
146 | fill: "#8E44AD",
147 | stroke: "#7E349D"
148 | }
149 | }
150 | },
151 | {
152 | id: 10,
153 | label: "current task",
154 | user: (
155 |
160 | Johnattan Owens
161 |
162 | ),
163 | start: getDate(24 * 5),
164 | duration: 24 * 60 * 60 * 1000,
165 | progress: 0,
166 | type: "task"
167 | }
168 | ];
169 |
170 | const options = {
171 | title: {
172 | label: "Your project title as html (link or whatever...)",
173 | html: false
174 | },
175 | times: {
176 | timeZoom: 10,
177 | firstTime: dayjs("2020/03/10").valueOf()
178 | },
179 | row: { height: 16 },
180 | taskList: {
181 | columns: [
182 | {
183 | id: 1,
184 | label: "ID",
185 | value: "id",
186 | width: 40
187 | },
188 | {
189 | id: 2,
190 | label: "Description",
191 | value: "label",
192 | width: 200,
193 | expander: true
194 | },
195 | {
196 | id: 3,
197 | label: "Assigned to",
198 | value: "user",
199 | width: 130,
200 | html: true
201 | },
202 | {
203 | id: 4,
204 | label: "Start",
205 | value: task => dayjs(task.start).format("YYYY-MM-DD"),
206 | width: 78
207 | },
208 | {
209 | id: 5,
210 | label: "Type",
211 | value: "type",
212 | width: 68
213 | },
214 | {
215 | id: 6,
216 | label: "%",
217 | value: "progress",
218 | width: 35,
219 | style: {
220 | "task-list-header-label": {
221 | textAlign: "center",
222 | width: "100%"
223 | },
224 | "task-list-item-value-container": {
225 | textAlign: "center"
226 | }
227 | }
228 | }
229 | ]
230 | }
231 | // locale: {
232 | // name: "pl", // name String
233 | // weekdays: "Poniedziałek_Wtorek_Środa_Czwartek_Piątek_Sobota_Niedziela".split(
234 | // "_"
235 | // ), // weekdays Array
236 | // weekdaysShort: "Pon_Wto_Śro_Czw_Pią_Sob_Nie".split("_"), // OPTIONAL, short weekdays Array, use first three letters if not provided
237 | // weekdaysMin: "Pn_Wt_Śr_Cz_Pt_So_Ni".split("_"), // OPTIONAL, min weekdays Array, use first two letters if not provided
238 | // months: "Styczeń_Luty_Marzec_Kwiecień_Maj_Czerwiec_Lipiec_Sierpień_Wrzesień_Październik_Listopad_Grudzień".split(
239 | // "_"
240 | // ), // months Array
241 | // monthsShort: "Sty_Lut_Mar_Kwi_Maj_Cze_Lip_Sie_Wrz_Paź_Lis_Gru".split("_"), // OPTIONAL, short months Array, use first three letters if not provided
242 | // ordinal: n => `${n}`, // ordinal Function (number) => return number + output
243 | // relativeTime: {
244 | // // relative time format strings, keep %s %d as the same
245 | // future: "za %s", // e.g. in 2 hours, %s been replaced with 2hours
246 | // past: "%s temu",
247 | // s: "kilka sekund",
248 | // m: "minutę",
249 | // mm: "%d minut",
250 | // h: "godzinę",
251 | // hh: "%d godzin", // e.g. 2 hours, %d been replaced with 2
252 | // d: "dzień",
253 | // dd: "%d dni",
254 | // M: "miesiąc",
255 | // MM: "%d miesięcy",
256 | // y: "rok",
257 | // yy: "%d lat"
258 | // }
259 | // }
260 | };
261 |
262 | ReactDOM.render(
263 | ,
270 | document.getElementById("root")
271 | );
272 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | [class^="gantt-elastic"],
2 | [class*=" gantt-elastic"] {
3 | box-sizing: border-box;
4 | }
5 | .gantt-elastic__main-view svg {
6 | display: block;
7 | }
8 | .gantt-elastic__grid-horizontal-line,
9 | .gantt-elastic__grid-vertical-line {
10 | stroke: #a0a0a0;
11 | stroke-width: 1;
12 | }
13 | foreignObject > * {
14 | margin: 0px;
15 | }
16 | .gantt-elastic .p-2 {
17 | padding: 10rem;
18 | }
19 | .gantt-elastic__main-view-main-container,
20 | .gantt-elastic__main-view-container {
21 | overflow: hidden;
22 | max-width: 100%;
23 | }
24 | .gantt-elastic__task-list-header-column:last-of-type {
25 | border-right: 1px solid #00000050;
26 | }
27 | .gantt-elastic__task-list-item:last-of-type {
28 | border-bottom: 1px solid #00000050;
29 | }
30 | .gantt-elastic__task-list-item-value-wrapper:hover {
31 | overflow: visible !important;
32 | }
33 | .gantt-elastic__task-list-item-value-wrapper:hover
34 | > .gantt-elastic__task-list-item-value-container {
35 | position: relative;
36 | overflow: visible !important;
37 | }
38 | .gantt-elastic__task-list-item-value-wrapper:hover
39 | > .gantt-elastic__task-list-item-value {
40 | position: absolute;
41 | }
42 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import { Task, TaskListColumnOption } from "./components/interfaces";
3 |
4 | declare interface DynamicStyle {
5 | [key: string]: {
6 | [key: string]: string | number;
7 | };
8 | }
9 |
10 | declare interface GanttElasticTask {
11 | id: string | number; // *
12 | parentId?: string | number;
13 | dependentOn?: string[] | number[];
14 | start: dayjs.ConfigType; // *
15 | startTime?: number;
16 | end?: dayjs.ConfigType;
17 | endTime?: number;
18 | label: string; // *
19 | duration: number; // *
20 | progress: number; // *
21 | type?: string;
22 | style?: DynamicStyle;
23 | collapsed?: boolean;
24 | events?: {
25 | [key: string]: (
26 | event: React.MouseEvent | React.TouchEvent,
27 | task: Task
28 | ) => void;
29 | };
30 | }
31 |
32 | declare interface GanttElasticOptions {
33 | scroll?: {
34 | dragXMoveMultiplier?: number; //*
35 | dragYMoveMultiplier?: number; //*
36 | };
37 | scope?: {
38 | //*
39 | before: number;
40 | after: number;
41 | };
42 | times?: {
43 | timeScale?: number;
44 | timeZoom?: number; //*
45 | firstTime?: number;
46 | lastTime?: number;
47 | timePerPixel?: number; // 计算值,设置无效
48 | stepDuration?: dayjs.UnitType;
49 | };
50 | row: {
51 | height?: number; //*
52 | };
53 | maxRows?: number; //*
54 | maxHeight?: number; //*
55 | chart?: {
56 | grid?: {
57 | horizontal: {
58 | gap: number; //*
59 | };
60 | };
61 | progress?: {
62 | width?: number; //*
63 | height?: number; //*
64 | pattern?: boolean;
65 | bar?: boolean;
66 | };
67 | text?: {
68 | offset?: number; //*
69 | xPadding?: number; //*
70 | display?: boolean; //*
71 | };
72 | expander?: {
73 | type?: string;
74 | display?: boolean; //*
75 | displayIfTaskListHidden?: boolean; //*
76 | offset?: number; //*
77 | size?: number;
78 | };
79 | };
80 | taskList?: {
81 | display?: boolean; //*
82 | resizeAfterThreshold?: boolean; //*
83 | widthThreshold?: number; //*
84 | columns?: GanttElasticTaskListColumn[];
85 | percent?: number; //*
86 | minWidth?: number;
87 | expander?: {
88 | type?: string;
89 | size?: number;
90 | columnWidth?: number;
91 | padding?: number;
92 | margin?: number;
93 | straight?: boolean;
94 | };
95 | };
96 | calendar?: GanttElasticCalendar;
97 | locale?: ILocale;
98 | }
99 |
100 | declare interface GanttElasticTaskListColumn {
101 | id: number;
102 | label: string;
103 | value: string | Function;
104 | style?: DynamicStyle;
105 | width: number;
106 | events?: {
107 | [key: string]: (a: {
108 | event: React.MouseEvent | React.TouchEvent;
109 | data: Task;
110 | column: TaskListColumnOption;
111 | }) => void;
112 | };
113 | expander?: boolean;
114 | }
115 |
116 | declare interface GanttElasticCalendar {
117 | workingDays?: number[]; //*
118 | gap?: number; //*
119 | strokeWidth?: number;
120 | hour?: {
121 | height?: number; //*
122 | display?: boolean; //*
123 | format?: DateFormat; //*
124 | };
125 | day?: {
126 | height?: number; //*
127 | display?: boolean; //*
128 | format?: DateFormat; //*
129 | };
130 | month?: {
131 | height?: number; //*
132 | display?: boolean; //*
133 | format?: DateFormat; //*
134 | };
135 | }
136 |
137 | declare interface DateFormat {
138 | long?: (date: dayjs.Dayjs) => string;
139 | medium?: (date: dayjs.Dayjs) => string;
140 | short?: (date: dayjs.Dayjs) => string;
141 | [key: string]: (date: dayjs.Dayjs) => string;
142 | }
143 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": false,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react",
17 | "sourceMap": true,
18 | "baseUrl": "./",
19 | "paths": {
20 | "@/*": ["src/*"]
21 | },
22 | //
23 | "noImplicitAny": true,
24 | "removeComments": true
25 | },
26 | "awesomeTypescriptLoaderOptions": {
27 | /* ... */
28 | }
29 | // "include": ["src"]
30 | }
31 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const TerserPlugin = require("terser-webpack-plugin");
3 | const { CheckerPlugin } = require("awesome-typescript-loader");
4 | const LodashModuleReplacementPlugin = require("lodash-webpack-plugin");
5 |
6 | module.exports = [
7 | {
8 | mode: "production",
9 | optimization: {
10 | minimize: false
11 | },
12 | entry: "./src/GanttElastic.tsx",
13 | output: {
14 | filename: "GanttElastic.js",
15 | // eslint-disable-next-line no-undef
16 | path: path.join(__dirname, "./dist"),
17 | library: "GanttElastic",
18 | libraryTarget: "commonjs2",
19 | libraryExport: "default"
20 | },
21 | // Enable sourcemaps for debugging webpack's output.
22 | devtool: "source-map",
23 | resolve: {
24 | // Add '.ts' and '.tsx' as resolvable extensions.
25 | extensions: [".ts", ".tsx", ".js", ".json"],
26 | alias: {
27 | // eslint-disable-next-line no-undef
28 | "@": path.join(__dirname, "src")
29 | }
30 | },
31 | module: {
32 | rules: [
33 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
34 | {
35 | test: /\.tsx?$/,
36 | exclude: /node_modules/,
37 | // loader: "awesome-typescript-loader",
38 | use: {
39 | loader: "babel-loader",
40 | options: {
41 | presets: ["@babel/preset-env"],
42 | plugins: ["lodash"]
43 | }
44 | }
45 | }
46 | ]
47 | },
48 | // When importing a module whose path matches one of the following, just
49 | // assume a corresponding global variable exists and use that instead.
50 | // This is important because it allows us to avoid bundling all of our
51 | // dependencies, which allows browsers to cache those libraries between builds.
52 | externals: [
53 | {
54 | react: "React",
55 | "react-dom": "ReactDOM"
56 | },
57 | /^(demo|\$)$/i
58 | ],
59 | plugins: [new CheckerPlugin(), new LodashModuleReplacementPlugin()]
60 | },
61 | {
62 | mode: "production",
63 | optimization: {
64 | minimize: true,
65 | namedModules: false,
66 | minimizer: [
67 | new TerserPlugin({
68 | terserOptions: {
69 | mangle: false
70 | }
71 | })
72 | ]
73 | },
74 | entry: "./src/GanttElastic.tsx",
75 | output: {
76 | filename: "GanttElastic.min.js",
77 | // eslint-disable-next-line no-undef
78 | path: path.join(__dirname, "./dist"),
79 | library: "GanttElastic",
80 | libraryTarget: "commonjs2",
81 | libraryExport: "default"
82 | },
83 | // Enable sourcemaps for debugging webpack's output.
84 | devtool: "source-map",
85 | resolve: {
86 | // Add '.ts' and '.tsx' as resolvable extensions.
87 | extensions: [".ts", ".tsx", ".js", ".json"],
88 | alias: {
89 | // eslint-disable-next-line no-undef
90 | "@": path.join(__dirname, "src")
91 | }
92 | },
93 | module: {
94 | rules: [
95 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
96 | {
97 | test: /\.tsx?$/,
98 | exclude: /node_modules/,
99 | // loader: "awesome-typescript-loader"
100 | use: {
101 | loader: "babel-loader",
102 | options: {
103 | presets: ["@babel/preset-env"],
104 | plugins: ["lodash"]
105 | }
106 | }
107 | }
108 | ]
109 | },
110 | // When importing a module whose path matches one of the following, just
111 | // assume a corresponding global variable exists and use that instead.
112 | // This is important because it allows us to avoid bundling all of our
113 | // dependencies, which allows browsers to cache those libraries between builds.
114 | externals: [
115 | {
116 | react: "React",
117 | "react-dom": "ReactDOM"
118 | },
119 | /^(demo|\$)$/i
120 | ],
121 | plugins: [new CheckerPlugin(), new LodashModuleReplacementPlugin()]
122 | }
123 | ];
124 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const path = require("path");
3 | const HtmlWebpackPlugin = require("html-webpack-plugin");
4 | const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
5 | const errorOverlayMiddleware = require("react-dev-utils/errorOverlayMiddleware");
6 | const evalSourceMapMiddleware = require("react-dev-utils/evalSourceMapMiddleware");
7 | const WatchMissingNodeModulesPlugin = require("react-dev-utils/WatchMissingNodeModulesPlugin");
8 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
9 |
10 | module.exports = {
11 | mode: "development",
12 | // Enable sourcemaps for debugging webpack's output.
13 | devtool: "source-map",
14 | entry: [
15 | "./src/demo.tsx",
16 | require.resolve("react-dev-utils/webpackHotDevClient")
17 | ],
18 | output: {
19 | // Add /* filename */ comments to generated require()s in the output.
20 | // pathinfo: true,
21 | // There will be one main bundle, and one file per asynchronous chunk.
22 | // In development, it does not produce real files.
23 | filename: "static/js/bundle.js",
24 | // There are also additional JS chunk files if you use code splitting.
25 | chunkFilename: "static/js/[name].chunk.js",
26 | // We inferred the "public path" (such as / or /my-project) from homepage.
27 | // We use "/" in development.
28 | publicPath: "/"
29 | },
30 | resolve: {
31 | // Add '.ts' and '.tsx' as resolvable extensions.
32 | extensions: [".ts", ".tsx", ".js", ".json"],
33 | alias: {
34 | // eslint-disable-next-line no-undef
35 | "@": path.join(__dirname, "src")
36 | }
37 | },
38 | devServer: {
39 | watchOptions: {
40 | aggregateTimeout: 300,
41 | poll: 1000
42 | },
43 | // Enable gzip compression of generated files.
44 | compress: true,
45 | // Silence WebpackDevServer's own logs since they're generally not useful.
46 | // It will still show compile warnings and errors with this setting.
47 | clientLogLevel: "info",
48 | // By default WebpackDevServer serves physical files from current directory
49 | // in addition to all the virtual build products that it serves from memory.
50 | // This is confusing because those files won’t automatically be available in
51 | // production build folder unless we copy them. However, copying the whole
52 | // project directory is dangerous because we may expose sensitive files.
53 | // Instead, we establish a convention that only files in `public` directory
54 | // get served. Our build script will copy `public` into the `build` folder.
55 | // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
56 | //
57 | // In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
58 | // Note that we only recommend to use `public` folder as an escape hatch
59 | // for files like `favicon.ico`, `manifest.json`, and libraries that are
60 | // for some reason broken when imported through Webpack. If you just want to
61 | // use an image, put it in `src` and `import` it from JavaScript instead.
62 | // contentBase: path.join(__dirname, "./dist"),
63 | // By default files from `contentBase` will not trigger a page reload.
64 | watchContentBase: true,
65 | // Enable hot reloading server. It will provide /sockjs-node/ endpoint
66 | // for the WebpackDevServer client so it can learn when the files were
67 | // updated. The WebpackDevServer client is included as an entry point
68 | // in the Webpack development configuration. Note that only changes
69 | // to CSS are currently hot reloaded. JS changes will refresh the browser.
70 | hot: true,
71 | port: 3000,
72 | // Use 'ws' instead of 'sockjs-node' on server since we're using native
73 | // websockets in `webpackHotDevClient`.
74 | transportMode: "ws",
75 | // Prevent a WS client from getting injected as we're already including
76 | // `webpackHotDevClient`.
77 | injectClient: false,
78 | // It is important to tell WebpackDevServer to use the same "root" path
79 | // as we specified in the config. In development, we always serve from /.
80 | publicPath: "/",
81 | before(app, server) {
82 | // This lets us fetch source contents from webpack for the error overlay
83 | app.use(evalSourceMapMiddleware(server));
84 | // This lets us open files from the runtime error overlay.
85 | app.use(errorOverlayMiddleware());
86 | // This service worker file is effectively a 'no-op' that will reset any
87 | // previous service worker registered for the same host:port combination.
88 | // We do this in development to avoid hitting the production cache if
89 | // it used the same host and port.
90 | // https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
91 | // app.use(noopServiceWorkerMiddleware());
92 | }
93 | },
94 | module: {
95 | rules: [
96 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
97 | // { test: /\.tsx?$/, loader: "awesome-typescript-loader" }
98 | {
99 | test: /\.tsx?$/,
100 | exclude: /(node_modules|bower_components)/,
101 | use: {
102 | loader: "babel-loader",
103 | options: {
104 | presets: ["@babel/preset-env"]
105 | }
106 | }
107 | },
108 | {
109 | test: /\.css$/,
110 | use: [
111 | {
112 | loader: MiniCssExtractPlugin.loader
113 | },
114 | "css-loader"
115 | ]
116 | }
117 | ]
118 | },
119 | plugins: [
120 | // Generates an `index.html` file with the