├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── example ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── components │ │ └── view-switcher.tsx │ ├── helper.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── setupTests.ts └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── .eslintrc ├── components │ ├── calendar │ │ ├── calendar.module.css │ │ ├── calendar.tsx │ │ └── top-part-of-calendar.tsx │ ├── gantt │ │ ├── gantt.module.css │ │ ├── gantt.tsx │ │ ├── task-gantt-content.tsx │ │ └── task-gantt.tsx │ ├── grid │ │ ├── grid-body.tsx │ │ ├── grid.module.css │ │ └── grid.tsx │ ├── other │ │ ├── arrow.tsx │ │ ├── horizontal-scroll.module.css │ │ ├── horizontal-scroll.tsx │ │ ├── tooltip.module.css │ │ ├── tooltip.tsx │ │ ├── vertical-scroll.module.css │ │ └── vertical-scroll.tsx │ ├── task-item │ │ ├── bar │ │ │ ├── bar-date-handle.tsx │ │ │ ├── bar-display.tsx │ │ │ ├── bar-progress-handle.tsx │ │ │ ├── bar-small.tsx │ │ │ ├── bar.module.css │ │ │ └── bar.tsx │ │ ├── milestone │ │ │ ├── milestone.module.css │ │ │ └── milestone.tsx │ │ ├── project │ │ │ ├── project.module.css │ │ │ └── project.tsx │ │ ├── task-item.tsx │ │ └── task-list.module.css │ └── task-list │ │ ├── task-list-header.module.css │ │ ├── task-list-header.tsx │ │ ├── task-list-table.module.css │ │ ├── task-list-table.tsx │ │ └── task-list.tsx ├── helpers │ ├── bar-helper.ts │ ├── date-helper.ts │ └── other-helper.ts ├── index.tsx ├── react-app-env.d.ts ├── test │ ├── date-helper.test.tsx │ └── gant.test.tsx ├── types │ ├── bar-task.ts │ ├── date-setup.ts │ ├── gantt-task-actions.ts │ └── public-types.ts └── typings.d.ts ├── tsconfig.json └── tsconfig.test.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js 6 | *.css -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["react-app", "react-app/jest"], 4 | "plugins": ["@typescript-eslint"], 5 | "env": { 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2020, 10 | "ecmaFeatures": { 11 | "legacyDecorators": true, 12 | "jsx": true 13 | } 14 | }, 15 | "settings": { 16 | "react": { 17 | "version": "16" 18 | } 19 | }, 20 | "rules": { 21 | "space-before-function-paren": 0, 22 | "react/prop-types": 0, 23 | "react/jsx-handler-names": 0, 24 | "react/jsx-fragments": 0, 25 | "react/no-unused-prop-types": 0, 26 | "import/export": 0, 27 | "no-unused-vars": "off", 28 | "no-use-before-define": "off", 29 | "@typescript-eslint/no-unused-vars": "error" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [published, edited] 6 | 7 | jobs: 8 | release: 9 | name: publish 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Begin CI... 14 | uses: actions/checkout@v2 15 | 16 | - name: Use Node 16 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 16.x 20 | registry-url: https://registry.npmjs.org 21 | 22 | - name: NPM Publish 23 | run: | 24 | if [ -e package-lock.json ]; then 25 | npm ci 26 | else 27 | npm i 28 | fi 29 | npm publish --access public 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "endOfLine": "auto" 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Maksym Vikarii https://github.com/MaTeMaTuK 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gantt-task-react 2 | 3 | ## Interactive Gantt Chart for React with TypeScript. 4 | 5 | ![example](https://user-images.githubusercontent.com/26743903/88215863-f35d5f00-cc64-11ea-81db-e829e6e9b5c8.png) 6 | 7 | ## [Live Demo](https://matematuk.github.io/gantt-task-react/) 8 | 9 | ## Install 10 | 11 | ``` 12 | npm install gantt-task-react 13 | ``` 14 | 15 | ## How to use it 16 | 17 | ```javascript 18 | import { Gantt, Task, EventOption, StylingOption, ViewMode, DisplayOption } from 'gantt-task-react'; 19 | import "gantt-task-react/dist/index.css"; 20 | 21 | let tasks: Task[] = [ 22 | { 23 | start: new Date(2020, 1, 1), 24 | end: new Date(2020, 1, 2), 25 | name: 'Idea', 26 | id: 'Task 0', 27 | type:'task', 28 | progress: 45, 29 | isDisabled: true, 30 | styles: { progressColor: '#ffbb54', progressSelectedColor: '#ff9e0d' }, 31 | }, 32 | ... 33 | ]; 34 | 35 | ``` 36 | 37 | You may handle actions 38 | 39 | ```javascript 40 | 49 | ``` 50 | 51 | ## How to run example 52 | 53 | ``` 54 | cd ./example 55 | npm install 56 | npm start 57 | ``` 58 | 59 | ## Gantt Configuration 60 | 61 | ### GanttProps 62 | 63 | | Parameter Name | Type | Description | 64 | | :------------------------------ | :------------ | :------------------------------------------------- | 65 | | tasks\* | [Task](#Task) | Tasks array. | 66 | | [EventOption](#EventOption) | interface | Specifies gantt events. | 67 | | [DisplayOption](#DisplayOption) | interface | Specifies view type and display timeline language. | 68 | | [StylingOption](#StylingOption) | interface | Specifies chart and global tasks styles | 69 | 70 | ### EventOption 71 | 72 | | Parameter Name | Type | Description | 73 | | :----------------- | :---------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | 74 | | onSelect | (task: Task, isSelected: boolean) => void | Specifies the function to be executed on the taskbar select or unselect event. | 75 | | onDoubleClick | (task: Task) => void | Specifies the function to be executed on the taskbar onDoubleClick event. | 76 | | onClick | (task: Task) => void | Specifies the function to be executed on the taskbar onClick event. | 77 | | onDelete\* | (task: Task) => void/boolean/Promise/Promise | Specifies the function to be executed on the taskbar on Delete button press event. | 78 | | onDateChange\* | (task: Task, children: Task[]) => void/boolean/Promise/Promise | Specifies the function to be executed when drag taskbar event on timeline has finished. | 79 | | onProgressChange\* | (task: Task, children: Task[]) => void/boolean/Promise/Promise | Specifies the function to be executed when drag taskbar progress event has finished. | 80 | | onExpanderClick\* | onExpanderClick: (task: Task) => void; | Specifies the function to be executed on the table expander click | 81 | | timeStep | number | A time step value for onDateChange. Specify in milliseconds. | 82 | 83 | \* Chart undoes operation if method return false or error. Parameter children returns one level deep records. 84 | 85 | ### DisplayOption 86 | 87 | | Parameter Name | Type | Description | 88 | | :------------- | :------ | :---------------------------------------------------------------------------------------------------------- | 89 | | viewMode | enum | Specifies the time scale. Hour, Quarter Day, Half Day, Day, Week(ISO-8601, 1st day is Monday), Month, QuarterYear, Year. | 90 | | viewDate | date | Specifies display date and time for display. | 91 | | preStepsCount | number | Specifies empty space before the fist task | 92 | | locale | string | Specifies the month name language. Able formats: ISO 639-2, Java Locale. | 93 | | rtl | boolean | Sets rtl mode. | 94 | 95 | ### StylingOption 96 | 97 | | Parameter Name | Type | Description | 98 | | :------------------------- | :----- | :--------------------------------------------------------------------------------------------- | 99 | | headerHeight | number | Specifies the header height. | 100 | | ganttHeight | number | Specifies the gantt chart height without header. Default is 0. It`s mean no height limitation. | 101 | | columnWidth | number | Specifies the time period width. | 102 | | listCellWidth | string | Specifies the task list cell width. Empty string is mean "no display". | 103 | | rowHeight | number | Specifies the task row height. | 104 | | barCornerRadius | number | Specifies the taskbar corner rounding. | 105 | | barFill | number | Specifies the taskbar occupation. Sets in percent from 0 to 100. | 106 | | handleWidth | number | Specifies width the taskbar drag event control for start and end dates. | 107 | | fontFamily | string | Specifies the application font. | 108 | | fontSize | string | Specifies the application font size. | 109 | | barProgressColor | string | Specifies the taskbar progress fill color globally. | 110 | | barProgressSelectedColor | string | Specifies the taskbar progress fill color globally on select. | 111 | | barBackgroundColor | string | Specifies the taskbar background fill color globally. | 112 | | barBackgroundSelectedColor | string | Specifies the taskbar background fill color globally on select. | 113 | | arrowColor | string | Specifies the relationship arrow fill color. | 114 | | arrowIndent | number | Specifies the relationship arrow right indent. Sets in px | 115 | | todayColor | string | Specifies the current period column fill color. | 116 | | TooltipContent | | Specifies the Tooltip view for selected taskbar. | 117 | | TaskListHeader | | Specifies the task list Header view | 118 | | TaskListTable | | Specifies the task list Table view | 119 | 120 | - TooltipContent: [`React.FC<{ task: Task; fontSize: string; fontFamily: string; }>;`](https://github.com/MaTeMaTuK/gantt-task-react/blob/main/src/components/other/tooltip.tsx#L56) 121 | - TaskListHeader: `React.FC<{ headerHeight: number; rowWidth: string; fontFamily: string; fontSize: string;}>;` 122 | - TaskListTable: `React.FC<{ rowHeight: number; rowWidth: string; fontFamily: string; fontSize: string; locale: string; tasks: Task[]; selectedTaskId: string; setSelectedTask: (taskId: string) => void; }>;` 123 | 124 | ### Task 125 | 126 | | Parameter Name | Type | Description | 127 | | :------------- | :------- | :---------------------------------------------------------------------------------------------------- | 128 | | id\* | string | Task id. | 129 | | name\* | string | Task display name. | 130 | | type\* | string | Task display type: **task**, **milestone**, **project** | 131 | | start\* | Date | Task start date. | 132 | | end\* | Date | Task end date. | 133 | | progress\* | number | Task progress. Sets in percent from 0 to 100. | 134 | | dependencies | string[] | Specifies the parent dependencies ids. | 135 | | styles | object | Specifies the taskbar styling settings locally. Object is passed with the following attributes: | 136 | | | | - **backgroundColor**: String. Specifies the taskbar background fill color locally. | 137 | | | | - **backgroundSelectedColor**: String. Specifies the taskbar background fill color locally on select. | 138 | | | | - **progressColor**: String. Specifies the taskbar progress fill color locally. | 139 | | | | - **progressSelectedColor**: String. Specifies the taskbar progress fill color globally on select. | 140 | | isDisabled | bool | Disables all action for current task. | 141 | | fontSize | string | Specifies the taskbar font size locally. | 142 | | project | string | Task project name | 143 | | hideChildren | bool | Hide children items. Parameter works with project type only | 144 | 145 | \*Required 146 | 147 | ## License 148 | 149 | [MIT](https://oss.ninja/mit/jaredpalmer/) 150 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | It is linked to the gantt-task-react package in the parent directory for development purposes. 4 | 5 | You can run `npm install` and then `npm start` to test your package. 6 | -------------------------------------------------------------------------------- /example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gantt-task-react-example", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gantt-task-react-example", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "@testing-library/jest-dom": "file:../node_modules/@testing-library/jest-dom", 12 | "@testing-library/react": "file:../node_modules/@testing-library/react", 13 | "@testing-library/user-event": "file:../node_modules/@testing-library/user-event", 14 | "@types/jest": "file:../node_modules/@types/jest", 15 | "@types/node": "file:../node_modules/@types/node", 16 | "@types/react": "file:../node_modules/@types/react", 17 | "@types/react-dom": "file:../node_modules/@types/react-dom", 18 | "gantt-task-react": "file:..", 19 | "postcss-preset-env": "file:../node_modules/postcss-preset-env", 20 | "react": "file:../node_modules/react", 21 | "react-dom": "file:../node_modules/react-dom", 22 | "react-scripts": "file:../node_modules/react-scripts" 23 | }, 24 | "devDependencies": {} 25 | }, 26 | "..": { 27 | "version": "0.3.9", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@testing-library/jest-dom": "^5.16.4", 31 | "@testing-library/react": "^13.3.0", 32 | "@testing-library/user-event": "^14.2.1", 33 | "@types/jest": "^27.5.1", 34 | "@types/node": "^15.0.1", 35 | "@types/react": "^18.0.5", 36 | "@types/react-dom": "^18.0.5", 37 | "cross-env": "^7.0.3", 38 | "gh-pages": "^3.1.0", 39 | "microbundle-crl": "^0.13.11", 40 | "mini-css-extract-plugin": "^2.5.1", 41 | "npm-run-all": "^4.1.5", 42 | "postcss-flexbugs-fixes": "^5.0.2", 43 | "postcss-normalize": "^10.0.1", 44 | "postcss-preset-env": "^7.6.0", 45 | "prettier": "^2.7.1", 46 | "react": "^18.2.0", 47 | "react-dom": "^18.2.0", 48 | "react-scripts": "^5.0.1", 49 | "typescript": "^4.7.4" 50 | }, 51 | "engines": { 52 | "node": ">=10" 53 | }, 54 | "peerDependencies": { 55 | "react": "^18.0.0" 56 | } 57 | }, 58 | "../node_modules/@testing-library/jest-dom": { 59 | "version": "5.16.4", 60 | "license": "MIT", 61 | "dependencies": { 62 | "@babel/runtime": "^7.9.2", 63 | "@types/testing-library__jest-dom": "^5.9.1", 64 | "aria-query": "^5.0.0", 65 | "chalk": "^3.0.0", 66 | "css": "^3.0.0", 67 | "css.escape": "^1.5.1", 68 | "dom-accessibility-api": "^0.5.6", 69 | "lodash": "^4.17.15", 70 | "redent": "^3.0.0" 71 | }, 72 | "engines": { 73 | "node": ">=8", 74 | "npm": ">=6", 75 | "yarn": ">=1" 76 | } 77 | }, 78 | "../node_modules/@testing-library/react": { 79 | "version": "11.2.7", 80 | "license": "MIT", 81 | "dependencies": { 82 | "@babel/runtime": "^7.12.5", 83 | "@testing-library/dom": "^7.28.1" 84 | }, 85 | "engines": { 86 | "node": ">=10" 87 | }, 88 | "peerDependencies": { 89 | "react": "*", 90 | "react-dom": "*" 91 | } 92 | }, 93 | "../node_modules/@testing-library/user-event": { 94 | "version": "13.5.0", 95 | "license": "MIT", 96 | "dependencies": { 97 | "@babel/runtime": "^7.12.5" 98 | }, 99 | "engines": { 100 | "node": ">=10", 101 | "npm": ">=6" 102 | }, 103 | "peerDependencies": { 104 | "@testing-library/dom": ">=7.21.4" 105 | } 106 | }, 107 | "../node_modules/@types/jest": { 108 | "version": "26.0.24", 109 | "license": "MIT", 110 | "dependencies": { 111 | "jest-diff": "^26.0.0", 112 | "pretty-format": "^26.0.0" 113 | } 114 | }, 115 | "../node_modules/@types/node": { 116 | "version": "15.14.9", 117 | "license": "MIT" 118 | }, 119 | "../node_modules/@types/react": { 120 | "version": "18.0.9", 121 | "license": "MIT", 122 | "dependencies": { 123 | "@types/prop-types": "*", 124 | "@types/scheduler": "*", 125 | "csstype": "^3.0.2" 126 | } 127 | }, 128 | "../node_modules/@types/react-dom": { 129 | "version": "18.0.4", 130 | "license": "MIT", 131 | "dependencies": { 132 | "@types/react": "*" 133 | } 134 | }, 135 | "../node_modules/postcss-preset-env": { 136 | "version": "7.5.0", 137 | "license": "CC0-1.0", 138 | "dependencies": { 139 | "@csstools/postcss-color-function": "^1.1.0", 140 | "@csstools/postcss-font-format-keywords": "^1.0.0", 141 | "@csstools/postcss-hwb-function": "^1.0.0", 142 | "@csstools/postcss-ic-unit": "^1.0.0", 143 | "@csstools/postcss-is-pseudo-class": "^2.0.2", 144 | "@csstools/postcss-normalize-display-values": "^1.0.0", 145 | "@csstools/postcss-oklab-function": "^1.1.0", 146 | "@csstools/postcss-progressive-custom-properties": "^1.3.0", 147 | "@csstools/postcss-stepped-value-functions": "^1.0.0", 148 | "@csstools/postcss-unset-value": "^1.0.0", 149 | "autoprefixer": "^10.4.6", 150 | "browserslist": "^4.20.3", 151 | "css-blank-pseudo": "^3.0.3", 152 | "css-has-pseudo": "^3.0.4", 153 | "css-prefers-color-scheme": "^6.0.3", 154 | "cssdb": "^6.6.1", 155 | "postcss-attribute-case-insensitive": "^5.0.0", 156 | "postcss-clamp": "^4.1.0", 157 | "postcss-color-functional-notation": "^4.2.2", 158 | "postcss-color-hex-alpha": "^8.0.3", 159 | "postcss-color-rebeccapurple": "^7.0.2", 160 | "postcss-custom-media": "^8.0.0", 161 | "postcss-custom-properties": "^12.1.7", 162 | "postcss-custom-selectors": "^6.0.0", 163 | "postcss-dir-pseudo-class": "^6.0.4", 164 | "postcss-double-position-gradients": "^3.1.1", 165 | "postcss-env-function": "^4.0.6", 166 | "postcss-focus-visible": "^6.0.4", 167 | "postcss-focus-within": "^5.0.4", 168 | "postcss-font-variant": "^5.0.0", 169 | "postcss-gap-properties": "^3.0.3", 170 | "postcss-image-set-function": "^4.0.6", 171 | "postcss-initial": "^4.0.1", 172 | "postcss-lab-function": "^4.2.0", 173 | "postcss-logical": "^5.0.4", 174 | "postcss-media-minmax": "^5.0.0", 175 | "postcss-nesting": "^10.1.4", 176 | "postcss-opacity-percentage": "^1.1.2", 177 | "postcss-overflow-shorthand": "^3.0.3", 178 | "postcss-page-break": "^3.0.4", 179 | "postcss-place": "^7.0.4", 180 | "postcss-pseudo-class-any-link": "^7.1.2", 181 | "postcss-replace-overflow-wrap": "^4.0.0", 182 | "postcss-selector-not": "^5.0.0", 183 | "postcss-value-parser": "^4.2.0" 184 | }, 185 | "engines": { 186 | "node": "^12 || ^14 || >=16" 187 | }, 188 | "funding": { 189 | "type": "opencollective", 190 | "url": "https://opencollective.com/csstools" 191 | }, 192 | "peerDependencies": { 193 | "postcss": "^8.4" 194 | } 195 | }, 196 | "../node_modules/react": { 197 | "version": "18.1.0", 198 | "license": "MIT", 199 | "dependencies": { 200 | "loose-envify": "^1.1.0" 201 | }, 202 | "engines": { 203 | "node": ">=0.10.0" 204 | } 205 | }, 206 | "../node_modules/react-dom": { 207 | "version": "18.1.0", 208 | "license": "MIT", 209 | "dependencies": { 210 | "loose-envify": "^1.1.0", 211 | "scheduler": "^0.22.0" 212 | }, 213 | "peerDependencies": { 214 | "react": "^18.1.0" 215 | } 216 | }, 217 | "../node_modules/react-scripts": { 218 | "version": "2.1.8", 219 | "license": "MIT", 220 | "dependencies": { 221 | "@babel/core": "7.2.2", 222 | "@svgr/webpack": "4.1.0", 223 | "babel-core": "7.0.0-bridge.0", 224 | "babel-eslint": "9.0.0", 225 | "babel-jest": "23.6.0", 226 | "babel-loader": "8.0.5", 227 | "babel-plugin-named-asset-import": "^0.3.1", 228 | "babel-preset-react-app": "^7.0.2", 229 | "bfj": "6.1.1", 230 | "case-sensitive-paths-webpack-plugin": "2.2.0", 231 | "css-loader": "1.0.0", 232 | "dotenv": "6.0.0", 233 | "dotenv-expand": "4.2.0", 234 | "eslint": "5.12.0", 235 | "eslint-config-react-app": "^3.0.8", 236 | "eslint-loader": "2.1.1", 237 | "eslint-plugin-flowtype": "2.50.1", 238 | "eslint-plugin-import": "2.14.0", 239 | "eslint-plugin-jsx-a11y": "6.1.2", 240 | "eslint-plugin-react": "7.12.4", 241 | "file-loader": "2.0.0", 242 | "fs-extra": "7.0.1", 243 | "html-webpack-plugin": "4.0.0-alpha.2", 244 | "identity-obj-proxy": "3.0.0", 245 | "jest": "23.6.0", 246 | "jest-pnp-resolver": "1.0.2", 247 | "jest-resolve": "23.6.0", 248 | "jest-watch-typeahead": "^0.2.1", 249 | "mini-css-extract-plugin": "0.5.0", 250 | "optimize-css-assets-webpack-plugin": "5.0.1", 251 | "pnp-webpack-plugin": "1.2.1", 252 | "postcss-flexbugs-fixes": "4.1.0", 253 | "postcss-loader": "3.0.0", 254 | "postcss-preset-env": "6.5.0", 255 | "postcss-safe-parser": "4.0.1", 256 | "react-app-polyfill": "^0.2.2", 257 | "react-dev-utils": "^8.0.0", 258 | "resolve": "1.10.0", 259 | "sass-loader": "7.1.0", 260 | "style-loader": "0.23.1", 261 | "terser-webpack-plugin": "1.2.2", 262 | "url-loader": "1.1.2", 263 | "webpack": "4.28.3", 264 | "webpack-dev-server": "3.1.14", 265 | "webpack-manifest-plugin": "2.0.4", 266 | "workbox-webpack-plugin": "3.6.3" 267 | }, 268 | "bin": { 269 | "react-scripts": "bin/react-scripts.js" 270 | }, 271 | "engines": { 272 | "node": ">=8.10" 273 | }, 274 | "optionalDependencies": { 275 | "fsevents": "1.2.4" 276 | } 277 | }, 278 | "node_modules/@testing-library/jest-dom": { 279 | "resolved": "../node_modules/@testing-library/jest-dom", 280 | "link": true 281 | }, 282 | "node_modules/@testing-library/react": { 283 | "resolved": "../node_modules/@testing-library/react", 284 | "link": true 285 | }, 286 | "node_modules/@testing-library/user-event": { 287 | "resolved": "../node_modules/@testing-library/user-event", 288 | "link": true 289 | }, 290 | "node_modules/@types/jest": { 291 | "resolved": "../node_modules/@types/jest", 292 | "link": true 293 | }, 294 | "node_modules/@types/node": { 295 | "resolved": "../node_modules/@types/node", 296 | "link": true 297 | }, 298 | "node_modules/@types/react": { 299 | "resolved": "../node_modules/@types/react", 300 | "link": true 301 | }, 302 | "node_modules/@types/react-dom": { 303 | "resolved": "../node_modules/@types/react-dom", 304 | "link": true 305 | }, 306 | "node_modules/gantt-task-react": { 307 | "resolved": "..", 308 | "link": true 309 | }, 310 | "node_modules/postcss-preset-env": { 311 | "resolved": "../node_modules/postcss-preset-env", 312 | "link": true 313 | }, 314 | "node_modules/react": { 315 | "resolved": "../node_modules/react", 316 | "link": true 317 | }, 318 | "node_modules/react-dom": { 319 | "resolved": "../node_modules/react-dom", 320 | "link": true 321 | }, 322 | "node_modules/react-scripts": { 323 | "resolved": "../node_modules/react-scripts", 324 | "link": true 325 | } 326 | }, 327 | "dependencies": { 328 | "@testing-library/jest-dom": { 329 | "version": "file:../node_modules/@testing-library/jest-dom", 330 | "requires": { 331 | "@babel/runtime": "^7.9.2", 332 | "@types/testing-library__jest-dom": "^5.9.1", 333 | "aria-query": "^5.0.0", 334 | "chalk": "^3.0.0", 335 | "css": "^3.0.0", 336 | "css.escape": "^1.5.1", 337 | "dom-accessibility-api": "^0.5.6", 338 | "lodash": "^4.17.15", 339 | "redent": "^3.0.0" 340 | } 341 | }, 342 | "@testing-library/react": { 343 | "version": "file:../node_modules/@testing-library/react", 344 | "requires": { 345 | "@babel/runtime": "^7.12.5", 346 | "@testing-library/dom": "^7.28.1" 347 | } 348 | }, 349 | "@testing-library/user-event": { 350 | "version": "file:../node_modules/@testing-library/user-event", 351 | "requires": { 352 | "@babel/runtime": "^7.12.5" 353 | } 354 | }, 355 | "@types/jest": { 356 | "version": "file:../node_modules/@types/jest", 357 | "requires": { 358 | "jest-diff": "^26.0.0", 359 | "pretty-format": "^26.0.0" 360 | } 361 | }, 362 | "@types/node": { 363 | "version": "file:../node_modules/@types/node" 364 | }, 365 | "@types/react": { 366 | "version": "file:../node_modules/@types/react", 367 | "requires": { 368 | "@types/prop-types": "*", 369 | "@types/scheduler": "*", 370 | "csstype": "^3.0.2" 371 | } 372 | }, 373 | "@types/react-dom": { 374 | "version": "file:../node_modules/@types/react-dom", 375 | "requires": { 376 | "@types/react": "*" 377 | } 378 | }, 379 | "gantt-task-react": { 380 | "version": "file:..", 381 | "requires": { 382 | "@testing-library/jest-dom": "^5.16.4", 383 | "@testing-library/react": "^13.3.0", 384 | "@testing-library/user-event": "^14.2.1", 385 | "@types/jest": "^27.5.1", 386 | "@types/node": "^15.0.1", 387 | "@types/react": "^18.0.5", 388 | "@types/react-dom": "^18.0.5", 389 | "cross-env": "^7.0.3", 390 | "gh-pages": "^3.1.0", 391 | "microbundle-crl": "^0.13.11", 392 | "mini-css-extract-plugin": "^2.5.1", 393 | "npm-run-all": "^4.1.5", 394 | "postcss-flexbugs-fixes": "^5.0.2", 395 | "postcss-normalize": "^10.0.1", 396 | "postcss-preset-env": "^7.6.0", 397 | "prettier": "^2.7.1", 398 | "react": "^18.2.0", 399 | "react-dom": "^18.2.0", 400 | "react-scripts": "^5.0.1", 401 | "typescript": "^4.7.4" 402 | }, 403 | "dependencies": { 404 | "@testing-library/jest-dom": { 405 | "version": "5.16.4", 406 | "requires": { 407 | "@babel/runtime": "^7.9.2", 408 | "@types/testing-library__jest-dom": "^5.9.1", 409 | "aria-query": "^5.0.0", 410 | "chalk": "^3.0.0", 411 | "css": "^3.0.0", 412 | "css.escape": "^1.5.1", 413 | "dom-accessibility-api": "^0.5.6", 414 | "lodash": "^4.17.15", 415 | "redent": "^3.0.0" 416 | } 417 | }, 418 | "@testing-library/react": { 419 | "version": "11.2.7", 420 | "requires": { 421 | "@babel/runtime": "^7.12.5", 422 | "@testing-library/dom": "^7.28.1" 423 | } 424 | }, 425 | "@testing-library/user-event": { 426 | "version": "13.5.0", 427 | "requires": { 428 | "@babel/runtime": "^7.12.5" 429 | } 430 | }, 431 | "@types/jest": { 432 | "version": "26.0.24", 433 | "requires": { 434 | "jest-diff": "^26.0.0", 435 | "pretty-format": "^26.0.0" 436 | } 437 | }, 438 | "@types/node": { 439 | "version": "15.14.9" 440 | }, 441 | "@types/react": { 442 | "version": "18.0.9", 443 | "requires": { 444 | "@types/prop-types": "*", 445 | "@types/scheduler": "*", 446 | "csstype": "^3.0.2" 447 | } 448 | }, 449 | "@types/react-dom": { 450 | "version": "18.0.4", 451 | "requires": { 452 | "@types/react": "*" 453 | } 454 | }, 455 | "postcss-preset-env": { 456 | "version": "7.5.0", 457 | "requires": { 458 | "@csstools/postcss-color-function": "^1.1.0", 459 | "@csstools/postcss-font-format-keywords": "^1.0.0", 460 | "@csstools/postcss-hwb-function": "^1.0.0", 461 | "@csstools/postcss-ic-unit": "^1.0.0", 462 | "@csstools/postcss-is-pseudo-class": "^2.0.2", 463 | "@csstools/postcss-normalize-display-values": "^1.0.0", 464 | "@csstools/postcss-oklab-function": "^1.1.0", 465 | "@csstools/postcss-progressive-custom-properties": "^1.3.0", 466 | "@csstools/postcss-stepped-value-functions": "^1.0.0", 467 | "@csstools/postcss-unset-value": "^1.0.0", 468 | "autoprefixer": "^10.4.6", 469 | "browserslist": "^4.20.3", 470 | "css-blank-pseudo": "^3.0.3", 471 | "css-has-pseudo": "^3.0.4", 472 | "css-prefers-color-scheme": "^6.0.3", 473 | "cssdb": "^6.6.1", 474 | "postcss-attribute-case-insensitive": "^5.0.0", 475 | "postcss-clamp": "^4.1.0", 476 | "postcss-color-functional-notation": "^4.2.2", 477 | "postcss-color-hex-alpha": "^8.0.3", 478 | "postcss-color-rebeccapurple": "^7.0.2", 479 | "postcss-custom-media": "^8.0.0", 480 | "postcss-custom-properties": "^12.1.7", 481 | "postcss-custom-selectors": "^6.0.0", 482 | "postcss-dir-pseudo-class": "^6.0.4", 483 | "postcss-double-position-gradients": "^3.1.1", 484 | "postcss-env-function": "^4.0.6", 485 | "postcss-focus-visible": "^6.0.4", 486 | "postcss-focus-within": "^5.0.4", 487 | "postcss-font-variant": "^5.0.0", 488 | "postcss-gap-properties": "^3.0.3", 489 | "postcss-image-set-function": "^4.0.6", 490 | "postcss-initial": "^4.0.1", 491 | "postcss-lab-function": "^4.2.0", 492 | "postcss-logical": "^5.0.4", 493 | "postcss-media-minmax": "^5.0.0", 494 | "postcss-nesting": "^10.1.4", 495 | "postcss-opacity-percentage": "^1.1.2", 496 | "postcss-overflow-shorthand": "^3.0.3", 497 | "postcss-page-break": "^3.0.4", 498 | "postcss-place": "^7.0.4", 499 | "postcss-pseudo-class-any-link": "^7.1.2", 500 | "postcss-replace-overflow-wrap": "^4.0.0", 501 | "postcss-selector-not": "^5.0.0", 502 | "postcss-value-parser": "^4.2.0" 503 | } 504 | }, 505 | "react": { 506 | "version": "18.1.0", 507 | "requires": { 508 | "loose-envify": "^1.1.0" 509 | } 510 | }, 511 | "react-dom": { 512 | "version": "18.1.0", 513 | "requires": { 514 | "loose-envify": "^1.1.0", 515 | "scheduler": "^0.22.0" 516 | } 517 | }, 518 | "react-scripts": { 519 | "version": "2.1.8", 520 | "requires": { 521 | "@babel/core": "7.2.2", 522 | "@svgr/webpack": "4.1.0", 523 | "babel-core": "7.0.0-bridge.0", 524 | "babel-eslint": "9.0.0", 525 | "babel-jest": "23.6.0", 526 | "babel-loader": "8.0.5", 527 | "babel-plugin-named-asset-import": "^0.3.1", 528 | "babel-preset-react-app": "^7.0.2", 529 | "bfj": "6.1.1", 530 | "case-sensitive-paths-webpack-plugin": "2.2.0", 531 | "css-loader": "1.0.0", 532 | "dotenv": "6.0.0", 533 | "dotenv-expand": "4.2.0", 534 | "eslint": "5.12.0", 535 | "eslint-config-react-app": "^3.0.8", 536 | "eslint-loader": "2.1.1", 537 | "eslint-plugin-flowtype": "2.50.1", 538 | "eslint-plugin-import": "2.14.0", 539 | "eslint-plugin-jsx-a11y": "6.1.2", 540 | "eslint-plugin-react": "7.12.4", 541 | "file-loader": "2.0.0", 542 | "fs-extra": "7.0.1", 543 | "fsevents": "1.2.4", 544 | "html-webpack-plugin": "4.0.0-alpha.2", 545 | "identity-obj-proxy": "3.0.0", 546 | "jest": "23.6.0", 547 | "jest-pnp-resolver": "1.0.2", 548 | "jest-resolve": "23.6.0", 549 | "jest-watch-typeahead": "^0.2.1", 550 | "mini-css-extract-plugin": "0.5.0", 551 | "optimize-css-assets-webpack-plugin": "5.0.1", 552 | "pnp-webpack-plugin": "1.2.1", 553 | "postcss-flexbugs-fixes": "4.1.0", 554 | "postcss-loader": "3.0.0", 555 | "postcss-preset-env": "6.5.0", 556 | "postcss-safe-parser": "4.0.1", 557 | "react-app-polyfill": "^0.2.2", 558 | "react-dev-utils": "^8.0.0", 559 | "resolve": "1.10.0", 560 | "sass-loader": "7.1.0", 561 | "style-loader": "0.23.1", 562 | "terser-webpack-plugin": "1.2.2", 563 | "url-loader": "1.1.2", 564 | "webpack": "4.28.3", 565 | "webpack-dev-server": "3.1.14", 566 | "webpack-manifest-plugin": "2.0.4", 567 | "workbox-webpack-plugin": "3.6.3" 568 | } 569 | } 570 | } 571 | }, 572 | "postcss-preset-env": { 573 | "version": "file:../node_modules/postcss-preset-env", 574 | "requires": { 575 | "@csstools/postcss-color-function": "^1.1.0", 576 | "@csstools/postcss-font-format-keywords": "^1.0.0", 577 | "@csstools/postcss-hwb-function": "^1.0.0", 578 | "@csstools/postcss-ic-unit": "^1.0.0", 579 | "@csstools/postcss-is-pseudo-class": "^2.0.2", 580 | "@csstools/postcss-normalize-display-values": "^1.0.0", 581 | "@csstools/postcss-oklab-function": "^1.1.0", 582 | "@csstools/postcss-progressive-custom-properties": "^1.3.0", 583 | "@csstools/postcss-stepped-value-functions": "^1.0.0", 584 | "@csstools/postcss-unset-value": "^1.0.0", 585 | "autoprefixer": "^10.4.6", 586 | "browserslist": "^4.20.3", 587 | "css-blank-pseudo": "^3.0.3", 588 | "css-has-pseudo": "^3.0.4", 589 | "css-prefers-color-scheme": "^6.0.3", 590 | "cssdb": "^6.6.1", 591 | "postcss-attribute-case-insensitive": "^5.0.0", 592 | "postcss-clamp": "^4.1.0", 593 | "postcss-color-functional-notation": "^4.2.2", 594 | "postcss-color-hex-alpha": "^8.0.3", 595 | "postcss-color-rebeccapurple": "^7.0.2", 596 | "postcss-custom-media": "^8.0.0", 597 | "postcss-custom-properties": "^12.1.7", 598 | "postcss-custom-selectors": "^6.0.0", 599 | "postcss-dir-pseudo-class": "^6.0.4", 600 | "postcss-double-position-gradients": "^3.1.1", 601 | "postcss-env-function": "^4.0.6", 602 | "postcss-focus-visible": "^6.0.4", 603 | "postcss-focus-within": "^5.0.4", 604 | "postcss-font-variant": "^5.0.0", 605 | "postcss-gap-properties": "^3.0.3", 606 | "postcss-image-set-function": "^4.0.6", 607 | "postcss-initial": "^4.0.1", 608 | "postcss-lab-function": "^4.2.0", 609 | "postcss-logical": "^5.0.4", 610 | "postcss-media-minmax": "^5.0.0", 611 | "postcss-nesting": "^10.1.4", 612 | "postcss-opacity-percentage": "^1.1.2", 613 | "postcss-overflow-shorthand": "^3.0.3", 614 | "postcss-page-break": "^3.0.4", 615 | "postcss-place": "^7.0.4", 616 | "postcss-pseudo-class-any-link": "^7.1.2", 617 | "postcss-replace-overflow-wrap": "^4.0.0", 618 | "postcss-selector-not": "^5.0.0", 619 | "postcss-value-parser": "^4.2.0" 620 | } 621 | }, 622 | "react": { 623 | "version": "file:../node_modules/react", 624 | "requires": { 625 | "loose-envify": "^1.1.0" 626 | } 627 | }, 628 | "react-dom": { 629 | "version": "file:../node_modules/react-dom", 630 | "requires": { 631 | "loose-envify": "^1.1.0", 632 | "scheduler": "^0.22.0" 633 | } 634 | }, 635 | "react-scripts": { 636 | "version": "file:../node_modules/react-scripts", 637 | "requires": { 638 | "@babel/core": "7.2.2", 639 | "@svgr/webpack": "4.1.0", 640 | "babel-core": "7.0.0-bridge.0", 641 | "babel-eslint": "9.0.0", 642 | "babel-jest": "23.6.0", 643 | "babel-loader": "8.0.5", 644 | "babel-plugin-named-asset-import": "^0.3.1", 645 | "babel-preset-react-app": "^7.0.2", 646 | "bfj": "6.1.1", 647 | "case-sensitive-paths-webpack-plugin": "2.2.0", 648 | "css-loader": "1.0.0", 649 | "dotenv": "6.0.0", 650 | "dotenv-expand": "4.2.0", 651 | "eslint": "5.12.0", 652 | "eslint-config-react-app": "^3.0.8", 653 | "eslint-loader": "2.1.1", 654 | "eslint-plugin-flowtype": "2.50.1", 655 | "eslint-plugin-import": "2.14.0", 656 | "eslint-plugin-jsx-a11y": "6.1.2", 657 | "eslint-plugin-react": "7.12.4", 658 | "file-loader": "2.0.0", 659 | "fs-extra": "7.0.1", 660 | "fsevents": "1.2.4", 661 | "html-webpack-plugin": "4.0.0-alpha.2", 662 | "identity-obj-proxy": "3.0.0", 663 | "jest": "23.6.0", 664 | "jest-pnp-resolver": "1.0.2", 665 | "jest-resolve": "23.6.0", 666 | "jest-watch-typeahead": "^0.2.1", 667 | "mini-css-extract-plugin": "0.5.0", 668 | "optimize-css-assets-webpack-plugin": "5.0.1", 669 | "pnp-webpack-plugin": "1.2.1", 670 | "postcss-flexbugs-fixes": "4.1.0", 671 | "postcss-loader": "3.0.0", 672 | "postcss-preset-env": "6.5.0", 673 | "postcss-safe-parser": "4.0.1", 674 | "react-app-polyfill": "^0.2.2", 675 | "react-dev-utils": "^8.0.0", 676 | "resolve": "1.10.0", 677 | "sass-loader": "7.1.0", 678 | "style-loader": "0.23.1", 679 | "terser-webpack-plugin": "1.2.2", 680 | "url-loader": "1.1.2", 681 | "webpack": "4.28.3", 682 | "webpack-dev-server": "3.1.14", 683 | "webpack-manifest-plugin": "2.0.4", 684 | "workbox-webpack-plugin": "3.6.3" 685 | } 686 | } 687 | } 688 | } 689 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gantt-task-react-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start", 8 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build", 9 | "test": "node ../node_modules/react-scripts/bin/react-scripts.js test", 10 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject" 11 | }, 12 | "dependencies": { 13 | "@testing-library/jest-dom": "file:../node_modules/@testing-library/jest-dom", 14 | "@testing-library/react": "file:../node_modules/@testing-library/react", 15 | "@testing-library/user-event": "file:../node_modules/@testing-library/user-event", 16 | "@types/jest": "file:../node_modules/@types/jest", 17 | "@types/node": "file:../node_modules/@types/node", 18 | "@types/react": "file:../node_modules/@types/react", 19 | "@types/react-dom": "file:../node_modules/@types/react-dom", 20 | "gantt-task-react": "file:..", 21 | "postcss-preset-env": "file:../node_modules/postcss-preset-env", 22 | "react": "file:../node_modules/react", 23 | "react-dom": "file:../node_modules/react-dom", 24 | "react-scripts": "file:../node_modules/react-scripts" 25 | }, 26 | "devDependencies": { 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaTeMaTuK/gantt-task-react/e287736e7af865492345c815ae88ab2784bfff91/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 18 | 27 | gantt-task-react 28 | 29 | 30 | 31 | 34 | 35 |
36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "gantt-task-react", 3 | "name": "gantt-task-react", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | const root = createRoot(div); 8 | root.render(); 9 | }); 10 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Task, ViewMode, Gantt } from "gantt-task-react"; 3 | import { ViewSwitcher } from "./components/view-switcher"; 4 | import { getStartEndDateForProject, initTasks } from "./helper"; 5 | import "gantt-task-react/dist/index.css"; 6 | 7 | // Init 8 | const App = () => { 9 | const [view, setView] = React.useState(ViewMode.Day); 10 | const [tasks, setTasks] = React.useState(initTasks()); 11 | const [isChecked, setIsChecked] = React.useState(true); 12 | let columnWidth = 65; 13 | if (view === ViewMode.Year) { 14 | columnWidth = 350; 15 | } else if (view === ViewMode.Month) { 16 | columnWidth = 300; 17 | } else if (view === ViewMode.Week) { 18 | columnWidth = 250; 19 | } 20 | 21 | const handleTaskChange = (task: Task) => { 22 | console.log("On date change Id:" + task.id); 23 | let newTasks = tasks.map(t => (t.id === task.id ? task : t)); 24 | if (task.project) { 25 | const [start, end] = getStartEndDateForProject(newTasks, task.project); 26 | const project = newTasks[newTasks.findIndex(t => t.id === task.project)]; 27 | if ( 28 | project.start.getTime() !== start.getTime() || 29 | project.end.getTime() !== end.getTime() 30 | ) { 31 | const changedProject = { ...project, start, end }; 32 | newTasks = newTasks.map(t => 33 | t.id === task.project ? changedProject : t 34 | ); 35 | } 36 | } 37 | setTasks(newTasks); 38 | }; 39 | 40 | const handleTaskDelete = (task: Task) => { 41 | const conf = window.confirm("Are you sure about " + task.name + " ?"); 42 | if (conf) { 43 | setTasks(tasks.filter(t => t.id !== task.id)); 44 | } 45 | return conf; 46 | }; 47 | 48 | const handleProgressChange = async (task: Task) => { 49 | setTasks(tasks.map(t => (t.id === task.id ? task : t))); 50 | console.log("On progress change Id:" + task.id); 51 | }; 52 | 53 | const handleDblClick = (task: Task) => { 54 | alert("On Double Click event Id:" + task.id); 55 | }; 56 | 57 | const handleClick = (task: Task) => { 58 | console.log("On Click event Id:" + task.id); 59 | }; 60 | 61 | const handleSelect = (task: Task, isSelected: boolean) => { 62 | console.log(task.name + " has " + (isSelected ? "selected" : "unselected")); 63 | }; 64 | 65 | const handleExpanderClick = (task: Task) => { 66 | setTasks(tasks.map(t => (t.id === task.id ? task : t))); 67 | console.log("On expander click Id:" + task.id); 68 | }; 69 | 70 | return ( 71 |
72 | setView(viewMode)} 74 | onViewListChange={setIsChecked} 75 | isChecked={isChecked} 76 | /> 77 |

Gantt With Unlimited Height

78 | 91 |

Gantt With Limited Height

92 | 106 |
107 | ); 108 | }; 109 | 110 | export default App; 111 | -------------------------------------------------------------------------------- /example/src/components/view-switcher.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "gantt-task-react/dist/index.css"; 3 | import { ViewMode } from "gantt-task-react"; 4 | type ViewSwitcherProps = { 5 | isChecked: boolean; 6 | onViewListChange: (isChecked: boolean) => void; 7 | onViewModeChange: (viewMode: ViewMode) => void; 8 | }; 9 | export const ViewSwitcher: React.FC = ({ 10 | onViewModeChange, 11 | onViewListChange, 12 | isChecked, 13 | }) => { 14 | return ( 15 |
16 | 22 | 28 | 34 | 37 | 43 | 49 | 55 | 61 |
62 | 70 | Show Task List 71 |
72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /example/src/helper.tsx: -------------------------------------------------------------------------------- 1 | import { Task } from "../../dist/types/public-types"; 2 | 3 | export function initTasks() { 4 | const currentDate = new Date(); 5 | const tasks: Task[] = [ 6 | { 7 | start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1), 8 | end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15), 9 | name: "Some Project", 10 | id: "ProjectSample", 11 | progress: 25, 12 | type: "project", 13 | hideChildren: false, 14 | displayOrder: 1, 15 | }, 16 | { 17 | start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1), 18 | end: new Date( 19 | currentDate.getFullYear(), 20 | currentDate.getMonth(), 21 | 2, 22 | 12, 23 | 28 24 | ), 25 | name: "Idea", 26 | id: "Task 0", 27 | progress: 45, 28 | type: "task", 29 | project: "ProjectSample", 30 | displayOrder: 2, 31 | }, 32 | { 33 | start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 2), 34 | end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4, 0, 0), 35 | name: "Research", 36 | id: "Task 1", 37 | progress: 25, 38 | dependencies: ["Task 0"], 39 | type: "task", 40 | project: "ProjectSample", 41 | displayOrder: 3, 42 | }, 43 | { 44 | start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4), 45 | end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8, 0, 0), 46 | name: "Discussion with team", 47 | id: "Task 2", 48 | progress: 10, 49 | dependencies: ["Task 1"], 50 | type: "task", 51 | project: "ProjectSample", 52 | displayOrder: 4, 53 | }, 54 | { 55 | start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8), 56 | end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 9, 0, 0), 57 | name: "Developing", 58 | id: "Task 3", 59 | progress: 2, 60 | dependencies: ["Task 2"], 61 | type: "task", 62 | project: "ProjectSample", 63 | displayOrder: 5, 64 | }, 65 | { 66 | start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8), 67 | end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 10), 68 | name: "Review", 69 | id: "Task 4", 70 | type: "task", 71 | progress: 70, 72 | dependencies: ["Task 2"], 73 | project: "ProjectSample", 74 | displayOrder: 6, 75 | }, 76 | { 77 | start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15), 78 | end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15), 79 | name: "Release", 80 | id: "Task 6", 81 | progress: currentDate.getMonth(), 82 | type: "milestone", 83 | dependencies: ["Task 4"], 84 | project: "ProjectSample", 85 | displayOrder: 7, 86 | }, 87 | { 88 | start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 18), 89 | end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 19), 90 | name: "Party Time", 91 | id: "Task 9", 92 | progress: 0, 93 | isDisabled: true, 94 | type: "task", 95 | }, 96 | ]; 97 | return tasks; 98 | } 99 | 100 | export function getStartEndDateForProject(tasks: Task[], projectId: string) { 101 | const projectTasks = tasks.filter(t => t.project === projectId); 102 | let start = projectTasks[0].start; 103 | let end = projectTasks[0].end; 104 | 105 | for (let i = 0; i < projectTasks.length; i++) { 106 | const task = projectTasks[i]; 107 | if (start.getTime() > task.start.getTime()) { 108 | start = task.start; 109 | } 110 | if (end.getTime() < task.end.getTime()) { 111 | end = task.end; 112 | } 113 | } 114 | return [start, end]; 115 | } 116 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | .Wrapper { 2 | margin-bottom: 2rem; 3 | } 4 | .ViewContainer { 5 | list-style: none; 6 | -ms-box-orient: horizontal; 7 | display: flex; 8 | -webkit-justify-content: flex-end; 9 | justify-content: flex-end; 10 | align-items: center; 11 | } 12 | 13 | .Button { 14 | background-color: #e7e7e7; 15 | color: black; 16 | border: none; 17 | padding: 7px 16px; 18 | text-decoration: none; 19 | margin: 4px 2px; 20 | cursor: pointer; 21 | font-size: 14px; 22 | text-align: center; 23 | } 24 | .Switch { 25 | margin: 4px 15px; 26 | font-size: 14px; 27 | font-family: "Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue"; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | } 32 | .Switch_Toggle { 33 | position: relative; 34 | display: inline-block; 35 | width: 60px; 36 | height: 30px; 37 | margin-right: 5px; 38 | } 39 | 40 | .Switch_Toggle input { 41 | opacity: 0; 42 | width: 0; 43 | height: 0; 44 | } 45 | 46 | .Slider { 47 | position: absolute; 48 | cursor: pointer; 49 | top: 0; 50 | left: 0; 51 | right: 0; 52 | bottom: 0; 53 | background-color: #ccc; 54 | -webkit-transition: 0.4s; 55 | transition: 0.4s; 56 | } 57 | 58 | .Slider:before { 59 | position: absolute; 60 | content: ""; 61 | height: 21px; 62 | width: 21px; 63 | left: 6px; 64 | bottom: 4px; 65 | background-color: white; 66 | -webkit-transition: 0.4s; 67 | transition: 0.4s; 68 | } 69 | 70 | input:checked + .Slider { 71 | background-color: #2196f3; 72 | } 73 | 74 | input:focus + .Slider { 75 | box-shadow: 0 0 1px #2196f3; 76 | } 77 | 78 | input:checked + .Slider:before { 79 | -webkit-transform: translateX(26px); 80 | -ms-transform: translateX(26px); 81 | transform: translateX(26px); 82 | } 83 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | import React from "react"; 4 | import { createRoot } from "react-dom/client"; 5 | import App from "./App"; 6 | 7 | const container = document.getElementById("root"); 8 | const root = createRoot(container!); 9 | root.render(); 10 | -------------------------------------------------------------------------------- /example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom/extend-expect"; 6 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "allowSyntheticDefaultImports": true, 22 | "target": "es5", 23 | "allowJs": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "resolveJsonModule": true, 28 | "isolatedModules": true, 29 | "noEmit": true, 30 | "noFallthroughCasesInSwitch": true 31 | }, 32 | "include": [ 33 | "src" 34 | ], 35 | "exclude": [ 36 | "node_modules", 37 | "build" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gantt-task-react", 3 | "version": "0.3.9", 4 | "description": "Interactive Gantt Chart for React with TypeScript.", 5 | "author": "MaTeMaTuK ", 6 | "homepage": "https://github.com/MaTeMaTuK/gantt-task-react", 7 | "license": "MIT", 8 | "repository": "MaTeMaTuK/gantt-task-react", 9 | "main": "dist/index.js", 10 | "module": "dist/index.modern.js", 11 | "source": "src/index.tsx", 12 | "engines": { 13 | "node": ">=10" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "gantt", 18 | "typescript", 19 | "chart", 20 | "svg", 21 | "gantt-chart", 22 | "gantt chart", 23 | "react-gantt", 24 | "task" 25 | ], 26 | "scripts": { 27 | "build": "microbundle-crl --no-compress --format modern,cjs", 28 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 29 | "prepare": "run-s build", 30 | "test": "run-s test:unit test:lint test:build", 31 | "test:build": "run-s build", 32 | "test:lint": "eslint --ext .tsx src/**/*", 33 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", 34 | "test:watch": "react-scripts test --env=jsdom", 35 | "predeploy": "cd example && npm install && npm run build", 36 | "deploy": "gh-pages -d example/build" 37 | }, 38 | "peerDependencies": { 39 | "react": "^18.0.0" 40 | }, 41 | "devDependencies": { 42 | "@testing-library/jest-dom": "^5.16.4", 43 | "@testing-library/react": "^13.3.0", 44 | "@testing-library/user-event": "^14.2.1", 45 | "@types/jest": "^27.5.1", 46 | "@types/node": "^15.0.1", 47 | "@types/react": "^18.0.5", 48 | "@types/react-dom": "^18.0.5", 49 | "cross-env": "^7.0.3", 50 | "gh-pages": "^3.1.0", 51 | "microbundle-crl": "^0.13.11", 52 | "mini-css-extract-plugin": "^2.5.1", 53 | "npm-run-all": "^4.1.5", 54 | "postcss-flexbugs-fixes": "^5.0.2", 55 | "postcss-normalize": "^10.0.1", 56 | "postcss-preset-env": "^7.6.0", 57 | "prettier": "^2.7.1", 58 | "react": "^18.2.0", 59 | "react-dom": "^18.2.0", 60 | "react-scripts": "^5.0.1", 61 | "typescript": "^4.7.4" 62 | }, 63 | "files": [ 64 | "dist" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/calendar/calendar.module.css: -------------------------------------------------------------------------------- 1 | .calendarBottomText { 2 | text-anchor: middle; 3 | fill: #333; 4 | -webkit-touch-callout: none; 5 | -webkit-user-select: none; 6 | -moz-user-select: none; 7 | -ms-user-select: none; 8 | user-select: none; 9 | pointer-events: none; 10 | } 11 | 12 | .calendarTopTick { 13 | stroke: #e6e4e4; 14 | } 15 | 16 | .calendarTopText { 17 | text-anchor: middle; 18 | fill: #555; 19 | -webkit-touch-callout: none; 20 | -webkit-user-select: none; 21 | -moz-user-select: none; 22 | -ms-user-select: none; 23 | user-select: none; 24 | pointer-events: none; 25 | } 26 | 27 | .calendarHeader { 28 | fill: #ffffff; 29 | stroke: #e0e0e0; 30 | stroke-width: 1.4; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/calendar/calendar.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactChild } from "react"; 2 | import { ViewMode } from "../../types/public-types"; 3 | import { TopPartOfCalendar } from "./top-part-of-calendar"; 4 | import { 5 | getCachedDateTimeFormat, 6 | getDaysInMonth, 7 | getLocalDayOfWeek, 8 | getLocaleMonth, 9 | getWeekNumberISO8601, 10 | } from "../../helpers/date-helper"; 11 | import { DateSetup } from "../../types/date-setup"; 12 | import styles from "./calendar.module.css"; 13 | 14 | export type CalendarProps = { 15 | dateSetup: DateSetup; 16 | locale: string; 17 | viewMode: ViewMode; 18 | rtl: boolean; 19 | headerHeight: number; 20 | columnWidth: number; 21 | fontFamily: string; 22 | fontSize: string; 23 | }; 24 | 25 | export const Calendar: React.FC = ({ 26 | dateSetup, 27 | locale, 28 | viewMode, 29 | rtl, 30 | headerHeight, 31 | columnWidth, 32 | fontFamily, 33 | fontSize, 34 | }) => { 35 | const getCalendarValuesForYear = () => { 36 | const topValues: ReactChild[] = []; 37 | const bottomValues: ReactChild[] = []; 38 | const topDefaultHeight = headerHeight * 0.5; 39 | for (let i = 0; i < dateSetup.dates.length; i++) { 40 | const date = dateSetup.dates[i]; 41 | const bottomValue = date.getFullYear(); 42 | bottomValues.push( 43 | 49 | {bottomValue} 50 | 51 | ); 52 | if ( 53 | i === 0 || 54 | date.getFullYear() !== dateSetup.dates[i - 1].getFullYear() 55 | ) { 56 | const topValue = date.getFullYear().toString(); 57 | let xText: number; 58 | if (rtl) { 59 | xText = (6 + i + date.getFullYear() + 1) * columnWidth; 60 | } else { 61 | xText = (6 + i - date.getFullYear()) * columnWidth; 62 | } 63 | topValues.push( 64 | 73 | ); 74 | } 75 | } 76 | return [topValues, bottomValues]; 77 | }; 78 | 79 | const getCalendarValuesForQuarterYear = () => { 80 | const topValues: ReactChild[] = []; 81 | const bottomValues: ReactChild[] = []; 82 | const topDefaultHeight = headerHeight * 0.5; 83 | for (let i = 0; i < dateSetup.dates.length; i++) { 84 | const date = dateSetup.dates[i]; 85 | // const bottomValue = getLocaleMonth(date, locale); 86 | const quarter = "Q" + Math.floor((date.getMonth() + 3) / 3); 87 | bottomValues.push( 88 | 94 | {quarter} 95 | 96 | ); 97 | if ( 98 | i === 0 || 99 | date.getFullYear() !== dateSetup.dates[i - 1].getFullYear() 100 | ) { 101 | const topValue = date.getFullYear().toString(); 102 | let xText: number; 103 | if (rtl) { 104 | xText = (6 + i + date.getMonth() + 1) * columnWidth; 105 | } else { 106 | xText = (6 + i - date.getMonth()) * columnWidth; 107 | } 108 | topValues.push( 109 | 118 | ); 119 | } 120 | } 121 | return [topValues, bottomValues]; 122 | }; 123 | 124 | const getCalendarValuesForMonth = () => { 125 | const topValues: ReactChild[] = []; 126 | const bottomValues: ReactChild[] = []; 127 | const topDefaultHeight = headerHeight * 0.5; 128 | for (let i = 0; i < dateSetup.dates.length; i++) { 129 | const date = dateSetup.dates[i]; 130 | const bottomValue = getLocaleMonth(date, locale); 131 | bottomValues.push( 132 | 138 | {bottomValue} 139 | 140 | ); 141 | if ( 142 | i === 0 || 143 | date.getFullYear() !== dateSetup.dates[i - 1].getFullYear() 144 | ) { 145 | const topValue = date.getFullYear().toString(); 146 | let xText: number; 147 | if (rtl) { 148 | xText = (6 + i + date.getMonth() + 1) * columnWidth; 149 | } else { 150 | xText = (6 + i - date.getMonth()) * columnWidth; 151 | } 152 | topValues.push( 153 | 162 | ); 163 | } 164 | } 165 | return [topValues, bottomValues]; 166 | }; 167 | 168 | const getCalendarValuesForWeek = () => { 169 | const topValues: ReactChild[] = []; 170 | const bottomValues: ReactChild[] = []; 171 | let weeksCount: number = 1; 172 | const topDefaultHeight = headerHeight * 0.5; 173 | const dates = dateSetup.dates; 174 | for (let i = dates.length - 1; i >= 0; i--) { 175 | const date = dates[i]; 176 | let topValue = ""; 177 | if (i === 0 || date.getMonth() !== dates[i - 1].getMonth()) { 178 | // top 179 | topValue = `${getLocaleMonth(date, locale)}, ${date.getFullYear()}`; 180 | } 181 | // bottom 182 | const bottomValue = `W${getWeekNumberISO8601(date)}`; 183 | 184 | bottomValues.push( 185 | 191 | {bottomValue} 192 | 193 | ); 194 | 195 | if (topValue) { 196 | // if last day is new month 197 | if (i !== dates.length - 1) { 198 | topValues.push( 199 | 208 | ); 209 | } 210 | weeksCount = 0; 211 | } 212 | weeksCount++; 213 | } 214 | return [topValues, bottomValues]; 215 | }; 216 | 217 | const getCalendarValuesForDay = () => { 218 | const topValues: ReactChild[] = []; 219 | const bottomValues: ReactChild[] = []; 220 | const topDefaultHeight = headerHeight * 0.5; 221 | const dates = dateSetup.dates; 222 | for (let i = 0; i < dates.length; i++) { 223 | const date = dates[i]; 224 | const bottomValue = `${getLocalDayOfWeek(date, locale, "short")}, ${date 225 | .getDate() 226 | .toString()}`; 227 | 228 | bottomValues.push( 229 | 235 | {bottomValue} 236 | 237 | ); 238 | if ( 239 | i + 1 !== dates.length && 240 | date.getMonth() !== dates[i + 1].getMonth() 241 | ) { 242 | const topValue = getLocaleMonth(date, locale); 243 | 244 | topValues.push( 245 | 259 | ); 260 | } 261 | } 262 | return [topValues, bottomValues]; 263 | }; 264 | 265 | const getCalendarValuesForPartOfDay = () => { 266 | const topValues: ReactChild[] = []; 267 | const bottomValues: ReactChild[] = []; 268 | const ticks = viewMode === ViewMode.HalfDay ? 2 : 4; 269 | const topDefaultHeight = headerHeight * 0.5; 270 | const dates = dateSetup.dates; 271 | for (let i = 0; i < dates.length; i++) { 272 | const date = dates[i]; 273 | const bottomValue = getCachedDateTimeFormat(locale, { 274 | hour: "numeric", 275 | }).format(date); 276 | 277 | bottomValues.push( 278 | 285 | {bottomValue} 286 | 287 | ); 288 | if (i === 0 || date.getDate() !== dates[i - 1].getDate()) { 289 | const topValue = `${getLocalDayOfWeek( 290 | date, 291 | locale, 292 | "short" 293 | )}, ${date.getDate()} ${getLocaleMonth(date, locale)}`; 294 | topValues.push( 295 | 304 | ); 305 | } 306 | } 307 | 308 | return [topValues, bottomValues]; 309 | }; 310 | 311 | const getCalendarValuesForHour = () => { 312 | const topValues: ReactChild[] = []; 313 | const bottomValues: ReactChild[] = []; 314 | const topDefaultHeight = headerHeight * 0.5; 315 | const dates = dateSetup.dates; 316 | for (let i = 0; i < dates.length; i++) { 317 | const date = dates[i]; 318 | const bottomValue = getCachedDateTimeFormat(locale, { 319 | hour: "numeric", 320 | }).format(date); 321 | 322 | bottomValues.push( 323 | 330 | {bottomValue} 331 | 332 | ); 333 | if (i !== 0 && date.getDate() !== dates[i - 1].getDate()) { 334 | const displayDate = dates[i - 1]; 335 | const topValue = `${getLocalDayOfWeek( 336 | displayDate, 337 | locale, 338 | "long" 339 | )}, ${displayDate.getDate()} ${getLocaleMonth(displayDate, locale)}`; 340 | const topPosition = (date.getHours() - 24) / 2; 341 | topValues.push( 342 | 351 | ); 352 | } 353 | } 354 | 355 | return [topValues, bottomValues]; 356 | }; 357 | 358 | let topValues: ReactChild[] = []; 359 | let bottomValues: ReactChild[] = []; 360 | switch (dateSetup.viewMode) { 361 | case ViewMode.Year: 362 | [topValues, bottomValues] = getCalendarValuesForYear(); 363 | break; 364 | case ViewMode.QuarterYear: 365 | [topValues, bottomValues] = getCalendarValuesForQuarterYear(); 366 | break; 367 | case ViewMode.Month: 368 | [topValues, bottomValues] = getCalendarValuesForMonth(); 369 | break; 370 | case ViewMode.Week: 371 | [topValues, bottomValues] = getCalendarValuesForWeek(); 372 | break; 373 | case ViewMode.Day: 374 | [topValues, bottomValues] = getCalendarValuesForDay(); 375 | break; 376 | case ViewMode.QuarterDay: 377 | case ViewMode.HalfDay: 378 | [topValues, bottomValues] = getCalendarValuesForPartOfDay(); 379 | break; 380 | case ViewMode.Hour: 381 | [topValues, bottomValues] = getCalendarValuesForHour(); 382 | } 383 | return ( 384 | 385 | 392 | {bottomValues} {topValues} 393 | 394 | ); 395 | }; 396 | -------------------------------------------------------------------------------- /src/components/calendar/top-part-of-calendar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./calendar.module.css"; 3 | 4 | type TopPartOfCalendarProps = { 5 | value: string; 6 | x1Line: number; 7 | y1Line: number; 8 | y2Line: number; 9 | xText: number; 10 | yText: number; 11 | }; 12 | 13 | export const TopPartOfCalendar: React.FC = ({ 14 | value, 15 | x1Line, 16 | y1Line, 17 | y2Line, 18 | xText, 19 | yText, 20 | }) => { 21 | return ( 22 | 23 | 31 | 37 | {value} 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/gantt/gantt.module.css: -------------------------------------------------------------------------------- 1 | .ganttVerticalContainer { 2 | overflow: hidden; 3 | font-size: 0; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .horizontalContainer { 9 | margin: 0; 10 | padding: 0; 11 | overflow: hidden; 12 | } 13 | 14 | .wrapper { 15 | display: flex; 16 | padding: 0; 17 | margin: 0; 18 | list-style: none; 19 | outline: none; 20 | position: relative; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/gantt/gantt.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | SyntheticEvent, 4 | useRef, 5 | useEffect, 6 | useMemo, 7 | } from "react"; 8 | import { ViewMode, GanttProps, Task } from "../../types/public-types"; 9 | import { GridProps } from "../grid/grid"; 10 | import { ganttDateRange, seedDates } from "../../helpers/date-helper"; 11 | import { CalendarProps } from "../calendar/calendar"; 12 | import { TaskGanttContentProps } from "./task-gantt-content"; 13 | import { TaskListHeaderDefault } from "../task-list/task-list-header"; 14 | import { TaskListTableDefault } from "../task-list/task-list-table"; 15 | import { StandardTooltipContent, Tooltip } from "../other/tooltip"; 16 | import { VerticalScroll } from "../other/vertical-scroll"; 17 | import { TaskListProps, TaskList } from "../task-list/task-list"; 18 | import { TaskGantt } from "./task-gantt"; 19 | import { BarTask } from "../../types/bar-task"; 20 | import { convertToBarTasks } from "../../helpers/bar-helper"; 21 | import { GanttEvent } from "../../types/gantt-task-actions"; 22 | import { DateSetup } from "../../types/date-setup"; 23 | import { HorizontalScroll } from "../other/horizontal-scroll"; 24 | import { removeHiddenTasks, sortTasks } from "../../helpers/other-helper"; 25 | import styles from "./gantt.module.css"; 26 | 27 | export const Gantt: React.FunctionComponent = ({ 28 | tasks, 29 | headerHeight = 50, 30 | columnWidth = 60, 31 | listCellWidth = "155px", 32 | rowHeight = 50, 33 | ganttHeight = 0, 34 | viewMode = ViewMode.Day, 35 | preStepsCount = 1, 36 | locale = "en-GB", 37 | barFill = 60, 38 | barCornerRadius = 3, 39 | barProgressColor = "#a3a3ff", 40 | barProgressSelectedColor = "#8282f5", 41 | barBackgroundColor = "#b8c2cc", 42 | barBackgroundSelectedColor = "#aeb8c2", 43 | projectProgressColor = "#7db59a", 44 | projectProgressSelectedColor = "#59a985", 45 | projectBackgroundColor = "#fac465", 46 | projectBackgroundSelectedColor = "#f7bb53", 47 | milestoneBackgroundColor = "#f1c453", 48 | milestoneBackgroundSelectedColor = "#f29e4c", 49 | rtl = false, 50 | handleWidth = 8, 51 | timeStep = 300000, 52 | arrowColor = "grey", 53 | fontFamily = "Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue", 54 | fontSize = "14px", 55 | arrowIndent = 20, 56 | todayColor = "rgba(252, 248, 227, 0.5)", 57 | viewDate, 58 | TooltipContent = StandardTooltipContent, 59 | TaskListHeader = TaskListHeaderDefault, 60 | TaskListTable = TaskListTableDefault, 61 | onDateChange, 62 | onProgressChange, 63 | onDoubleClick, 64 | onClick, 65 | onDelete, 66 | onSelect, 67 | onExpanderClick, 68 | }) => { 69 | const wrapperRef = useRef(null); 70 | const taskListRef = useRef(null); 71 | const [dateSetup, setDateSetup] = useState(() => { 72 | const [startDate, endDate] = ganttDateRange(tasks, viewMode, preStepsCount); 73 | return { viewMode, dates: seedDates(startDate, endDate, viewMode) }; 74 | }); 75 | const [currentViewDate, setCurrentViewDate] = useState( 76 | undefined 77 | ); 78 | 79 | const [taskListWidth, setTaskListWidth] = useState(0); 80 | const [svgContainerWidth, setSvgContainerWidth] = useState(0); 81 | const [svgContainerHeight, setSvgContainerHeight] = useState(ganttHeight); 82 | const [barTasks, setBarTasks] = useState([]); 83 | const [ganttEvent, setGanttEvent] = useState({ 84 | action: "", 85 | }); 86 | const taskHeight = useMemo( 87 | () => (rowHeight * barFill) / 100, 88 | [rowHeight, barFill] 89 | ); 90 | 91 | const [selectedTask, setSelectedTask] = useState(); 92 | const [failedTask, setFailedTask] = useState(null); 93 | 94 | const svgWidth = dateSetup.dates.length * columnWidth; 95 | const ganttFullHeight = barTasks.length * rowHeight; 96 | 97 | const [scrollY, setScrollY] = useState(0); 98 | const [scrollX, setScrollX] = useState(-1); 99 | const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false); 100 | 101 | // task change events 102 | useEffect(() => { 103 | let filteredTasks: Task[]; 104 | if (onExpanderClick) { 105 | filteredTasks = removeHiddenTasks(tasks); 106 | } else { 107 | filteredTasks = tasks; 108 | } 109 | filteredTasks = filteredTasks.sort(sortTasks); 110 | const [startDate, endDate] = ganttDateRange( 111 | filteredTasks, 112 | viewMode, 113 | preStepsCount 114 | ); 115 | let newDates = seedDates(startDate, endDate, viewMode); 116 | if (rtl) { 117 | newDates = newDates.reverse(); 118 | if (scrollX === -1) { 119 | setScrollX(newDates.length * columnWidth); 120 | } 121 | } 122 | setDateSetup({ dates: newDates, viewMode }); 123 | setBarTasks( 124 | convertToBarTasks( 125 | filteredTasks, 126 | newDates, 127 | columnWidth, 128 | rowHeight, 129 | taskHeight, 130 | barCornerRadius, 131 | handleWidth, 132 | rtl, 133 | barProgressColor, 134 | barProgressSelectedColor, 135 | barBackgroundColor, 136 | barBackgroundSelectedColor, 137 | projectProgressColor, 138 | projectProgressSelectedColor, 139 | projectBackgroundColor, 140 | projectBackgroundSelectedColor, 141 | milestoneBackgroundColor, 142 | milestoneBackgroundSelectedColor 143 | ) 144 | ); 145 | }, [ 146 | tasks, 147 | viewMode, 148 | preStepsCount, 149 | rowHeight, 150 | barCornerRadius, 151 | columnWidth, 152 | taskHeight, 153 | handleWidth, 154 | barProgressColor, 155 | barProgressSelectedColor, 156 | barBackgroundColor, 157 | barBackgroundSelectedColor, 158 | projectProgressColor, 159 | projectProgressSelectedColor, 160 | projectBackgroundColor, 161 | projectBackgroundSelectedColor, 162 | milestoneBackgroundColor, 163 | milestoneBackgroundSelectedColor, 164 | rtl, 165 | scrollX, 166 | onExpanderClick, 167 | ]); 168 | 169 | useEffect(() => { 170 | if ( 171 | viewMode === dateSetup.viewMode && 172 | ((viewDate && !currentViewDate) || 173 | (viewDate && currentViewDate?.valueOf() !== viewDate.valueOf())) 174 | ) { 175 | const dates = dateSetup.dates; 176 | const index = dates.findIndex( 177 | (d, i) => 178 | viewDate.valueOf() >= d.valueOf() && 179 | i + 1 !== dates.length && 180 | viewDate.valueOf() < dates[i + 1].valueOf() 181 | ); 182 | if (index === -1) { 183 | return; 184 | } 185 | setCurrentViewDate(viewDate); 186 | setScrollX(columnWidth * index); 187 | } 188 | }, [ 189 | viewDate, 190 | columnWidth, 191 | dateSetup.dates, 192 | dateSetup.viewMode, 193 | viewMode, 194 | currentViewDate, 195 | setCurrentViewDate, 196 | ]); 197 | 198 | useEffect(() => { 199 | const { changedTask, action } = ganttEvent; 200 | if (changedTask) { 201 | if (action === "delete") { 202 | setGanttEvent({ action: "" }); 203 | setBarTasks(barTasks.filter(t => t.id !== changedTask.id)); 204 | } else if ( 205 | action === "move" || 206 | action === "end" || 207 | action === "start" || 208 | action === "progress" 209 | ) { 210 | const prevStateTask = barTasks.find(t => t.id === changedTask.id); 211 | if ( 212 | prevStateTask && 213 | (prevStateTask.start.getTime() !== changedTask.start.getTime() || 214 | prevStateTask.end.getTime() !== changedTask.end.getTime() || 215 | prevStateTask.progress !== changedTask.progress) 216 | ) { 217 | // actions for change 218 | const newTaskList = barTasks.map(t => 219 | t.id === changedTask.id ? changedTask : t 220 | ); 221 | setBarTasks(newTaskList); 222 | } 223 | } 224 | } 225 | }, [ganttEvent, barTasks]); 226 | 227 | useEffect(() => { 228 | if (failedTask) { 229 | setBarTasks(barTasks.map(t => (t.id !== failedTask.id ? t : failedTask))); 230 | setFailedTask(null); 231 | } 232 | }, [failedTask, barTasks]); 233 | 234 | useEffect(() => { 235 | if (!listCellWidth) { 236 | setTaskListWidth(0); 237 | } 238 | if (taskListRef.current) { 239 | setTaskListWidth(taskListRef.current.offsetWidth); 240 | } 241 | }, [taskListRef, listCellWidth]); 242 | 243 | useEffect(() => { 244 | if (wrapperRef.current) { 245 | setSvgContainerWidth(wrapperRef.current.offsetWidth - taskListWidth); 246 | } 247 | }, [wrapperRef, taskListWidth]); 248 | 249 | useEffect(() => { 250 | if (ganttHeight) { 251 | setSvgContainerHeight(ganttHeight + headerHeight); 252 | } else { 253 | setSvgContainerHeight(tasks.length * rowHeight + headerHeight); 254 | } 255 | }, [ganttHeight, tasks, headerHeight, rowHeight]); 256 | 257 | // scroll events 258 | useEffect(() => { 259 | const handleWheel = (event: WheelEvent) => { 260 | if (event.shiftKey || event.deltaX) { 261 | const scrollMove = event.deltaX ? event.deltaX : event.deltaY; 262 | let newScrollX = scrollX + scrollMove; 263 | if (newScrollX < 0) { 264 | newScrollX = 0; 265 | } else if (newScrollX > svgWidth) { 266 | newScrollX = svgWidth; 267 | } 268 | setScrollX(newScrollX); 269 | event.preventDefault(); 270 | } else if (ganttHeight) { 271 | let newScrollY = scrollY + event.deltaY; 272 | if (newScrollY < 0) { 273 | newScrollY = 0; 274 | } else if (newScrollY > ganttFullHeight - ganttHeight) { 275 | newScrollY = ganttFullHeight - ganttHeight; 276 | } 277 | if (newScrollY !== scrollY) { 278 | setScrollY(newScrollY); 279 | event.preventDefault(); 280 | } 281 | } 282 | 283 | setIgnoreScrollEvent(true); 284 | }; 285 | 286 | // subscribe if scroll is necessary 287 | wrapperRef.current?.addEventListener("wheel", handleWheel, { 288 | passive: false, 289 | }); 290 | return () => { 291 | wrapperRef.current?.removeEventListener("wheel", handleWheel); 292 | }; 293 | }, [ 294 | wrapperRef, 295 | scrollY, 296 | scrollX, 297 | ganttHeight, 298 | svgWidth, 299 | rtl, 300 | ganttFullHeight, 301 | ]); 302 | 303 | const handleScrollY = (event: SyntheticEvent) => { 304 | if (scrollY !== event.currentTarget.scrollTop && !ignoreScrollEvent) { 305 | setScrollY(event.currentTarget.scrollTop); 306 | setIgnoreScrollEvent(true); 307 | } else { 308 | setIgnoreScrollEvent(false); 309 | } 310 | }; 311 | 312 | const handleScrollX = (event: SyntheticEvent) => { 313 | if (scrollX !== event.currentTarget.scrollLeft && !ignoreScrollEvent) { 314 | setScrollX(event.currentTarget.scrollLeft); 315 | setIgnoreScrollEvent(true); 316 | } else { 317 | setIgnoreScrollEvent(false); 318 | } 319 | }; 320 | 321 | /** 322 | * Handles arrow keys events and transform it to new scroll 323 | */ 324 | const handleKeyDown = (event: React.KeyboardEvent) => { 325 | event.preventDefault(); 326 | let newScrollY = scrollY; 327 | let newScrollX = scrollX; 328 | let isX = true; 329 | switch (event.key) { 330 | case "Down": // IE/Edge specific value 331 | case "ArrowDown": 332 | newScrollY += rowHeight; 333 | isX = false; 334 | break; 335 | case "Up": // IE/Edge specific value 336 | case "ArrowUp": 337 | newScrollY -= rowHeight; 338 | isX = false; 339 | break; 340 | case "Left": 341 | case "ArrowLeft": 342 | newScrollX -= columnWidth; 343 | break; 344 | case "Right": // IE/Edge specific value 345 | case "ArrowRight": 346 | newScrollX += columnWidth; 347 | break; 348 | } 349 | if (isX) { 350 | if (newScrollX < 0) { 351 | newScrollX = 0; 352 | } else if (newScrollX > svgWidth) { 353 | newScrollX = svgWidth; 354 | } 355 | setScrollX(newScrollX); 356 | } else { 357 | if (newScrollY < 0) { 358 | newScrollY = 0; 359 | } else if (newScrollY > ganttFullHeight - ganttHeight) { 360 | newScrollY = ganttFullHeight - ganttHeight; 361 | } 362 | setScrollY(newScrollY); 363 | } 364 | setIgnoreScrollEvent(true); 365 | }; 366 | 367 | /** 368 | * Task select event 369 | */ 370 | const handleSelectedTask = (taskId: string) => { 371 | const newSelectedTask = barTasks.find(t => t.id === taskId); 372 | const oldSelectedTask = barTasks.find( 373 | t => !!selectedTask && t.id === selectedTask.id 374 | ); 375 | if (onSelect) { 376 | if (oldSelectedTask) { 377 | onSelect(oldSelectedTask, false); 378 | } 379 | if (newSelectedTask) { 380 | onSelect(newSelectedTask, true); 381 | } 382 | } 383 | setSelectedTask(newSelectedTask); 384 | }; 385 | const handleExpanderClick = (task: Task) => { 386 | if (onExpanderClick && task.hideChildren !== undefined) { 387 | onExpanderClick({ ...task, hideChildren: !task.hideChildren }); 388 | } 389 | }; 390 | const gridProps: GridProps = { 391 | columnWidth, 392 | svgWidth, 393 | tasks: tasks, 394 | rowHeight, 395 | dates: dateSetup.dates, 396 | todayColor, 397 | rtl, 398 | }; 399 | const calendarProps: CalendarProps = { 400 | dateSetup, 401 | locale, 402 | viewMode, 403 | headerHeight, 404 | columnWidth, 405 | fontFamily, 406 | fontSize, 407 | rtl, 408 | }; 409 | const barProps: TaskGanttContentProps = { 410 | tasks: barTasks, 411 | dates: dateSetup.dates, 412 | ganttEvent, 413 | selectedTask, 414 | rowHeight, 415 | taskHeight, 416 | columnWidth, 417 | arrowColor, 418 | timeStep, 419 | fontFamily, 420 | fontSize, 421 | arrowIndent, 422 | svgWidth, 423 | rtl, 424 | setGanttEvent, 425 | setFailedTask, 426 | setSelectedTask: handleSelectedTask, 427 | onDateChange, 428 | onProgressChange, 429 | onDoubleClick, 430 | onClick, 431 | onDelete, 432 | }; 433 | 434 | const tableProps: TaskListProps = { 435 | rowHeight, 436 | rowWidth: listCellWidth, 437 | fontFamily, 438 | fontSize, 439 | tasks: barTasks, 440 | locale, 441 | headerHeight, 442 | scrollY, 443 | ganttHeight, 444 | horizontalContainerClass: styles.horizontalContainer, 445 | selectedTask, 446 | taskListRef, 447 | setSelectedTask: handleSelectedTask, 448 | onExpanderClick: handleExpanderClick, 449 | TaskListHeader, 450 | TaskListTable, 451 | }; 452 | return ( 453 |
454 |
460 | {listCellWidth && } 461 | 469 | {ganttEvent.changedTask && ( 470 | 486 | )} 487 | 495 |
496 | 503 |
504 | ); 505 | }; 506 | -------------------------------------------------------------------------------- /src/components/gantt/task-gantt-content.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { EventOption } from "../../types/public-types"; 3 | import { BarTask } from "../../types/bar-task"; 4 | import { Arrow } from "../other/arrow"; 5 | import { handleTaskBySVGMouseEvent } from "../../helpers/bar-helper"; 6 | import { isKeyboardEvent } from "../../helpers/other-helper"; 7 | import { TaskItem } from "../task-item/task-item"; 8 | import { 9 | BarMoveAction, 10 | GanttContentMoveAction, 11 | GanttEvent, 12 | } from "../../types/gantt-task-actions"; 13 | 14 | export type TaskGanttContentProps = { 15 | tasks: BarTask[]; 16 | dates: Date[]; 17 | ganttEvent: GanttEvent; 18 | selectedTask: BarTask | undefined; 19 | rowHeight: number; 20 | columnWidth: number; 21 | timeStep: number; 22 | svg?: React.RefObject; 23 | svgWidth: number; 24 | taskHeight: number; 25 | arrowColor: string; 26 | arrowIndent: number; 27 | fontSize: string; 28 | fontFamily: string; 29 | rtl: boolean; 30 | setGanttEvent: (value: GanttEvent) => void; 31 | setFailedTask: (value: BarTask | null) => void; 32 | setSelectedTask: (taskId: string) => void; 33 | } & EventOption; 34 | 35 | export const TaskGanttContent: React.FC = ({ 36 | tasks, 37 | dates, 38 | ganttEvent, 39 | selectedTask, 40 | rowHeight, 41 | columnWidth, 42 | timeStep, 43 | svg, 44 | taskHeight, 45 | arrowColor, 46 | arrowIndent, 47 | fontFamily, 48 | fontSize, 49 | rtl, 50 | setGanttEvent, 51 | setFailedTask, 52 | setSelectedTask, 53 | onDateChange, 54 | onProgressChange, 55 | onDoubleClick, 56 | onClick, 57 | onDelete, 58 | }) => { 59 | const point = svg?.current?.createSVGPoint(); 60 | const [xStep, setXStep] = useState(0); 61 | const [initEventX1Delta, setInitEventX1Delta] = useState(0); 62 | const [isMoving, setIsMoving] = useState(false); 63 | 64 | // create xStep 65 | useEffect(() => { 66 | const dateDelta = 67 | dates[1].getTime() - 68 | dates[0].getTime() - 69 | dates[1].getTimezoneOffset() * 60 * 1000 + 70 | dates[0].getTimezoneOffset() * 60 * 1000; 71 | const newXStep = (timeStep * columnWidth) / dateDelta; 72 | setXStep(newXStep); 73 | }, [columnWidth, dates, timeStep]); 74 | 75 | useEffect(() => { 76 | const handleMouseMove = async (event: MouseEvent) => { 77 | if (!ganttEvent.changedTask || !point || !svg?.current) return; 78 | event.preventDefault(); 79 | 80 | point.x = event.clientX; 81 | const cursor = point.matrixTransform( 82 | svg?.current.getScreenCTM()?.inverse() 83 | ); 84 | 85 | const { isChanged, changedTask } = handleTaskBySVGMouseEvent( 86 | cursor.x, 87 | ganttEvent.action as BarMoveAction, 88 | ganttEvent.changedTask, 89 | xStep, 90 | timeStep, 91 | initEventX1Delta, 92 | rtl 93 | ); 94 | if (isChanged) { 95 | setGanttEvent({ action: ganttEvent.action, changedTask }); 96 | } 97 | }; 98 | 99 | const handleMouseUp = async (event: MouseEvent) => { 100 | const { action, originalSelectedTask, changedTask } = ganttEvent; 101 | if (!changedTask || !point || !svg?.current || !originalSelectedTask) 102 | return; 103 | event.preventDefault(); 104 | 105 | point.x = event.clientX; 106 | const cursor = point.matrixTransform( 107 | svg?.current.getScreenCTM()?.inverse() 108 | ); 109 | const { changedTask: newChangedTask } = handleTaskBySVGMouseEvent( 110 | cursor.x, 111 | action as BarMoveAction, 112 | changedTask, 113 | xStep, 114 | timeStep, 115 | initEventX1Delta, 116 | rtl 117 | ); 118 | 119 | const isNotLikeOriginal = 120 | originalSelectedTask.start !== newChangedTask.start || 121 | originalSelectedTask.end !== newChangedTask.end || 122 | originalSelectedTask.progress !== newChangedTask.progress; 123 | 124 | // remove listeners 125 | svg.current.removeEventListener("mousemove", handleMouseMove); 126 | svg.current.removeEventListener("mouseup", handleMouseUp); 127 | setGanttEvent({ action: "" }); 128 | setIsMoving(false); 129 | 130 | // custom operation start 131 | let operationSuccess = true; 132 | if ( 133 | (action === "move" || action === "end" || action === "start") && 134 | onDateChange && 135 | isNotLikeOriginal 136 | ) { 137 | try { 138 | const result = await onDateChange( 139 | newChangedTask, 140 | newChangedTask.barChildren 141 | ); 142 | if (result !== undefined) { 143 | operationSuccess = result; 144 | } 145 | } catch (error) { 146 | operationSuccess = false; 147 | } 148 | } else if (onProgressChange && isNotLikeOriginal) { 149 | try { 150 | const result = await onProgressChange( 151 | newChangedTask, 152 | newChangedTask.barChildren 153 | ); 154 | if (result !== undefined) { 155 | operationSuccess = result; 156 | } 157 | } catch (error) { 158 | operationSuccess = false; 159 | } 160 | } 161 | 162 | // If operation is failed - return old state 163 | if (!operationSuccess) { 164 | setFailedTask(originalSelectedTask); 165 | } 166 | }; 167 | 168 | if ( 169 | !isMoving && 170 | (ganttEvent.action === "move" || 171 | ganttEvent.action === "end" || 172 | ganttEvent.action === "start" || 173 | ganttEvent.action === "progress") && 174 | svg?.current 175 | ) { 176 | svg.current.addEventListener("mousemove", handleMouseMove); 177 | svg.current.addEventListener("mouseup", handleMouseUp); 178 | setIsMoving(true); 179 | } 180 | }, [ 181 | ganttEvent, 182 | xStep, 183 | initEventX1Delta, 184 | onProgressChange, 185 | timeStep, 186 | onDateChange, 187 | svg, 188 | isMoving, 189 | point, 190 | rtl, 191 | setFailedTask, 192 | setGanttEvent, 193 | ]); 194 | 195 | /** 196 | * Method is Start point of task change 197 | */ 198 | const handleBarEventStart = async ( 199 | action: GanttContentMoveAction, 200 | task: BarTask, 201 | event?: React.MouseEvent | React.KeyboardEvent 202 | ) => { 203 | if (!event) { 204 | if (action === "select") { 205 | setSelectedTask(task.id); 206 | } 207 | } 208 | // Keyboard events 209 | else if (isKeyboardEvent(event)) { 210 | if (action === "delete") { 211 | if (onDelete) { 212 | try { 213 | const result = await onDelete(task); 214 | if (result !== undefined && result) { 215 | setGanttEvent({ action, changedTask: task }); 216 | } 217 | } catch (error) { 218 | console.error("Error on Delete. " + error); 219 | } 220 | } 221 | } 222 | } 223 | // Mouse Events 224 | else if (action === "mouseenter") { 225 | if (!ganttEvent.action) { 226 | setGanttEvent({ 227 | action, 228 | changedTask: task, 229 | originalSelectedTask: task, 230 | }); 231 | } 232 | } else if (action === "mouseleave") { 233 | if (ganttEvent.action === "mouseenter") { 234 | setGanttEvent({ action: "" }); 235 | } 236 | } else if (action === "dblclick") { 237 | !!onDoubleClick && onDoubleClick(task); 238 | } else if (action === "click") { 239 | !!onClick && onClick(task); 240 | } 241 | // Change task event start 242 | else if (action === "move") { 243 | if (!svg?.current || !point) return; 244 | point.x = event.clientX; 245 | const cursor = point.matrixTransform( 246 | svg.current.getScreenCTM()?.inverse() 247 | ); 248 | setInitEventX1Delta(cursor.x - task.x1); 249 | setGanttEvent({ 250 | action, 251 | changedTask: task, 252 | originalSelectedTask: task, 253 | }); 254 | } else { 255 | setGanttEvent({ 256 | action, 257 | changedTask: task, 258 | originalSelectedTask: task, 259 | }); 260 | } 261 | }; 262 | 263 | return ( 264 | 265 | 266 | {tasks.map(task => { 267 | return task.barChildren.map(child => { 268 | return ( 269 | 278 | ); 279 | }); 280 | })} 281 | 282 | 283 | {tasks.map(task => { 284 | return ( 285 | 297 | ); 298 | })} 299 | 300 | 301 | ); 302 | }; 303 | -------------------------------------------------------------------------------- /src/components/gantt/task-gantt.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import { GridProps, Grid } from "../grid/grid"; 3 | import { CalendarProps, Calendar } from "../calendar/calendar"; 4 | import { TaskGanttContentProps, TaskGanttContent } from "./task-gantt-content"; 5 | import styles from "./gantt.module.css"; 6 | 7 | export type TaskGanttProps = { 8 | gridProps: GridProps; 9 | calendarProps: CalendarProps; 10 | barProps: TaskGanttContentProps; 11 | ganttHeight: number; 12 | scrollY: number; 13 | scrollX: number; 14 | }; 15 | export const TaskGantt: React.FC = ({ 16 | gridProps, 17 | calendarProps, 18 | barProps, 19 | ganttHeight, 20 | scrollY, 21 | scrollX, 22 | }) => { 23 | const ganttSVGRef = useRef(null); 24 | const horizontalContainerRef = useRef(null); 25 | const verticalGanttContainerRef = useRef(null); 26 | const newBarProps = { ...barProps, svg: ganttSVGRef }; 27 | 28 | useEffect(() => { 29 | if (horizontalContainerRef.current) { 30 | horizontalContainerRef.current.scrollTop = scrollY; 31 | } 32 | }, [scrollY]); 33 | 34 | useEffect(() => { 35 | if (verticalGanttContainerRef.current) { 36 | verticalGanttContainerRef.current.scrollLeft = scrollX; 37 | } 38 | }, [scrollX]); 39 | 40 | return ( 41 |
46 | 52 | 53 | 54 |
63 | 70 | 71 | 72 | 73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/grid/grid-body.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactChild } from "react"; 2 | import { Task } from "../../types/public-types"; 3 | import { addToDate } from "../../helpers/date-helper"; 4 | import styles from "./grid.module.css"; 5 | 6 | export type GridBodyProps = { 7 | tasks: Task[]; 8 | dates: Date[]; 9 | svgWidth: number; 10 | rowHeight: number; 11 | columnWidth: number; 12 | todayColor: string; 13 | rtl: boolean; 14 | }; 15 | export const GridBody: React.FC = ({ 16 | tasks, 17 | dates, 18 | rowHeight, 19 | svgWidth, 20 | columnWidth, 21 | todayColor, 22 | rtl, 23 | }) => { 24 | let y = 0; 25 | const gridRows: ReactChild[] = []; 26 | const rowLines: ReactChild[] = [ 27 | , 35 | ]; 36 | for (const task of tasks) { 37 | gridRows.push( 38 | 46 | ); 47 | rowLines.push( 48 | 56 | ); 57 | y += rowHeight; 58 | } 59 | 60 | const now = new Date(); 61 | let tickX = 0; 62 | const ticks: ReactChild[] = []; 63 | let today: ReactChild = ; 64 | for (let i = 0; i < dates.length; i++) { 65 | const date = dates[i]; 66 | ticks.push( 67 | 75 | ); 76 | if ( 77 | (i + 1 !== dates.length && 78 | date.getTime() < now.getTime() && 79 | dates[i + 1].getTime() >= now.getTime()) || 80 | // if current date is last 81 | (i !== 0 && 82 | i + 1 === dates.length && 83 | date.getTime() < now.getTime() && 84 | addToDate( 85 | date, 86 | date.getTime() - dates[i - 1].getTime(), 87 | "millisecond" 88 | ).getTime() >= now.getTime()) 89 | ) { 90 | today = ( 91 | 98 | ); 99 | } 100 | // rtl for today 101 | if ( 102 | rtl && 103 | i + 1 !== dates.length && 104 | date.getTime() >= now.getTime() && 105 | dates[i + 1].getTime() < now.getTime() 106 | ) { 107 | today = ( 108 | 115 | ); 116 | } 117 | tickX += columnWidth; 118 | } 119 | return ( 120 | 121 | {gridRows} 122 | {rowLines} 123 | {ticks} 124 | {today} 125 | 126 | ); 127 | }; 128 | -------------------------------------------------------------------------------- /src/components/grid/grid.module.css: -------------------------------------------------------------------------------- 1 | .gridRow { 2 | fill: #fff; 3 | } 4 | 5 | .gridRow:nth-child(even) { 6 | fill: #f5f5f5; 7 | } 8 | 9 | .gridRowLine { 10 | stroke: #ebeff2; 11 | } 12 | 13 | .gridTick { 14 | stroke: #e6e4e4; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/grid/grid.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GridBody, GridBodyProps } from "./grid-body"; 3 | 4 | export type GridProps = GridBodyProps; 5 | export const Grid: React.FC = props => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/other/arrow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BarTask } from "../../types/bar-task"; 3 | 4 | type ArrowProps = { 5 | taskFrom: BarTask; 6 | taskTo: BarTask; 7 | rowHeight: number; 8 | taskHeight: number; 9 | arrowIndent: number; 10 | rtl: boolean; 11 | }; 12 | export const Arrow: React.FC = ({ 13 | taskFrom, 14 | taskTo, 15 | rowHeight, 16 | taskHeight, 17 | arrowIndent, 18 | rtl, 19 | }) => { 20 | let path: string; 21 | let trianglePoints: string; 22 | if (rtl) { 23 | [path, trianglePoints] = drownPathAndTriangleRTL( 24 | taskFrom, 25 | taskTo, 26 | rowHeight, 27 | taskHeight, 28 | arrowIndent 29 | ); 30 | } else { 31 | [path, trianglePoints] = drownPathAndTriangle( 32 | taskFrom, 33 | taskTo, 34 | rowHeight, 35 | taskHeight, 36 | arrowIndent 37 | ); 38 | } 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | const drownPathAndTriangle = ( 49 | taskFrom: BarTask, 50 | taskTo: BarTask, 51 | rowHeight: number, 52 | taskHeight: number, 53 | arrowIndent: number 54 | ) => { 55 | const indexCompare = taskFrom.index > taskTo.index ? -1 : 1; 56 | const taskToEndPosition = taskTo.y + taskHeight / 2; 57 | const taskFromEndPosition = taskFrom.x2 + arrowIndent * 2; 58 | const taskFromHorizontalOffsetValue = 59 | taskFromEndPosition < taskTo.x1 ? "" : `H ${taskTo.x1 - arrowIndent}`; 60 | const taskToHorizontalOffsetValue = 61 | taskFromEndPosition > taskTo.x1 62 | ? arrowIndent 63 | : taskTo.x1 - taskFrom.x2 - arrowIndent; 64 | 65 | const path = `M ${taskFrom.x2} ${taskFrom.y + taskHeight / 2} 66 | h ${arrowIndent} 67 | v ${(indexCompare * rowHeight) / 2} 68 | ${taskFromHorizontalOffsetValue} 69 | V ${taskToEndPosition} 70 | h ${taskToHorizontalOffsetValue}`; 71 | 72 | const trianglePoints = `${taskTo.x1},${taskToEndPosition} 73 | ${taskTo.x1 - 5},${taskToEndPosition - 5} 74 | ${taskTo.x1 - 5},${taskToEndPosition + 5}`; 75 | return [path, trianglePoints]; 76 | }; 77 | 78 | const drownPathAndTriangleRTL = ( 79 | taskFrom: BarTask, 80 | taskTo: BarTask, 81 | rowHeight: number, 82 | taskHeight: number, 83 | arrowIndent: number 84 | ) => { 85 | const indexCompare = taskFrom.index > taskTo.index ? -1 : 1; 86 | const taskToEndPosition = taskTo.y + taskHeight / 2; 87 | const taskFromEndPosition = taskFrom.x1 - arrowIndent * 2; 88 | const taskFromHorizontalOffsetValue = 89 | taskFromEndPosition > taskTo.x2 ? "" : `H ${taskTo.x2 + arrowIndent}`; 90 | const taskToHorizontalOffsetValue = 91 | taskFromEndPosition < taskTo.x2 92 | ? -arrowIndent 93 | : taskTo.x2 - taskFrom.x1 + arrowIndent; 94 | 95 | const path = `M ${taskFrom.x1} ${taskFrom.y + taskHeight / 2} 96 | h ${-arrowIndent} 97 | v ${(indexCompare * rowHeight) / 2} 98 | ${taskFromHorizontalOffsetValue} 99 | V ${taskToEndPosition} 100 | h ${taskToHorizontalOffsetValue}`; 101 | 102 | const trianglePoints = `${taskTo.x2},${taskToEndPosition} 103 | ${taskTo.x2 + 5},${taskToEndPosition + 5} 104 | ${taskTo.x2 + 5},${taskToEndPosition - 5}`; 105 | return [path, trianglePoints]; 106 | }; 107 | -------------------------------------------------------------------------------- /src/components/other/horizontal-scroll.module.css: -------------------------------------------------------------------------------- 1 | .scrollWrapper { 2 | overflow: auto; 3 | max-width: 100%; 4 | /*firefox*/ 5 | scrollbar-width: thin; 6 | /*iPad*/ 7 | height: 1.2rem; 8 | } 9 | .scrollWrapper::-webkit-scrollbar { 10 | width: 1.1rem; 11 | height: 1.1rem; 12 | } 13 | .scrollWrapper::-webkit-scrollbar-corner { 14 | background: transparent; 15 | } 16 | .scrollWrapper::-webkit-scrollbar-thumb { 17 | border: 6px solid transparent; 18 | background: rgba(0, 0, 0, 0.2); 19 | background: var(--palette-black-alpha-20, rgba(0, 0, 0, 0.2)); 20 | border-radius: 10px; 21 | background-clip: padding-box; 22 | } 23 | .scrollWrapper::-webkit-scrollbar-thumb:hover { 24 | border: 4px solid transparent; 25 | background: rgba(0, 0, 0, 0.3); 26 | background: var(--palette-black-alpha-30, rgba(0, 0, 0, 0.3)); 27 | background-clip: padding-box; 28 | } 29 | @media only screen and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) { 30 | } 31 | .scroll { 32 | height: 1px; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/other/horizontal-scroll.tsx: -------------------------------------------------------------------------------- 1 | import React, { SyntheticEvent, useRef, useEffect } from "react"; 2 | import styles from "./horizontal-scroll.module.css"; 3 | 4 | export const HorizontalScroll: React.FC<{ 5 | scroll: number; 6 | svgWidth: number; 7 | taskListWidth: number; 8 | rtl: boolean; 9 | onScroll: (event: SyntheticEvent) => void; 10 | }> = ({ scroll, svgWidth, taskListWidth, rtl, onScroll }) => { 11 | const scrollRef = useRef(null); 12 | 13 | useEffect(() => { 14 | if (scrollRef.current) { 15 | scrollRef.current.scrollLeft = scroll; 16 | } 17 | }, [scroll]); 18 | 19 | return ( 20 |
31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/other/tooltip.module.css: -------------------------------------------------------------------------------- 1 | .tooltipDefaultContainer { 2 | background: #fff; 3 | padding: 12px; 4 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); 5 | } 6 | 7 | .tooltipDefaultContainerParagraph { 8 | font-size: 12px; 9 | margin-bottom: 6px; 10 | color: #666; 11 | } 12 | 13 | .tooltipDetailsContainer { 14 | position: absolute; 15 | display: flex; 16 | flex-shrink: 0; 17 | pointer-events: none; 18 | -webkit-touch-callout: none; 19 | -webkit-user-select: none; 20 | -moz-user-select: none; 21 | -ms-user-select: none; 22 | user-select: none; 23 | } 24 | 25 | .tooltipDetailsContainerHidden { 26 | visibility: hidden; 27 | position: absolute; 28 | display: flex; 29 | pointer-events: none; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/other/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import { Task } from "../../types/public-types"; 3 | import { BarTask } from "../../types/bar-task"; 4 | import styles from "./tooltip.module.css"; 5 | 6 | export type TooltipProps = { 7 | task: BarTask; 8 | arrowIndent: number; 9 | rtl: boolean; 10 | svgContainerHeight: number; 11 | svgContainerWidth: number; 12 | svgWidth: number; 13 | headerHeight: number; 14 | taskListWidth: number; 15 | scrollX: number; 16 | scrollY: number; 17 | rowHeight: number; 18 | fontSize: string; 19 | fontFamily: string; 20 | TooltipContent: React.FC<{ 21 | task: Task; 22 | fontSize: string; 23 | fontFamily: string; 24 | }>; 25 | }; 26 | export const Tooltip: React.FC = ({ 27 | task, 28 | rowHeight, 29 | rtl, 30 | svgContainerHeight, 31 | svgContainerWidth, 32 | scrollX, 33 | scrollY, 34 | arrowIndent, 35 | fontSize, 36 | fontFamily, 37 | headerHeight, 38 | taskListWidth, 39 | TooltipContent, 40 | }) => { 41 | const tooltipRef = useRef(null); 42 | const [relatedY, setRelatedY] = useState(0); 43 | const [relatedX, setRelatedX] = useState(0); 44 | useEffect(() => { 45 | if (tooltipRef.current) { 46 | const tooltipHeight = tooltipRef.current.offsetHeight * 1.1; 47 | const tooltipWidth = tooltipRef.current.offsetWidth * 1.1; 48 | 49 | let newRelatedY = task.index * rowHeight - scrollY + headerHeight; 50 | let newRelatedX: number; 51 | if (rtl) { 52 | newRelatedX = task.x1 - arrowIndent * 1.5 - tooltipWidth - scrollX; 53 | if (newRelatedX < 0) { 54 | newRelatedX = task.x2 + arrowIndent * 1.5 - scrollX; 55 | } 56 | const tooltipLeftmostPoint = tooltipWidth + newRelatedX; 57 | if (tooltipLeftmostPoint > svgContainerWidth) { 58 | newRelatedX = svgContainerWidth - tooltipWidth; 59 | newRelatedY += rowHeight; 60 | } 61 | } else { 62 | newRelatedX = task.x2 + arrowIndent * 1.5 + taskListWidth - scrollX; 63 | const tooltipLeftmostPoint = tooltipWidth + newRelatedX; 64 | const fullChartWidth = taskListWidth + svgContainerWidth; 65 | if (tooltipLeftmostPoint > fullChartWidth) { 66 | newRelatedX = 67 | task.x1 + 68 | taskListWidth - 69 | arrowIndent * 1.5 - 70 | scrollX - 71 | tooltipWidth; 72 | } 73 | if (newRelatedX < taskListWidth) { 74 | newRelatedX = svgContainerWidth + taskListWidth - tooltipWidth; 75 | newRelatedY += rowHeight; 76 | } 77 | } 78 | 79 | const tooltipLowerPoint = tooltipHeight + newRelatedY - scrollY; 80 | if (tooltipLowerPoint > svgContainerHeight - scrollY) { 81 | newRelatedY = svgContainerHeight - tooltipHeight; 82 | } 83 | setRelatedY(newRelatedY); 84 | setRelatedX(newRelatedX); 85 | } 86 | }, [ 87 | tooltipRef, 88 | task, 89 | arrowIndent, 90 | scrollX, 91 | scrollY, 92 | headerHeight, 93 | taskListWidth, 94 | rowHeight, 95 | svgContainerHeight, 96 | svgContainerWidth, 97 | rtl, 98 | ]); 99 | 100 | return ( 101 |
110 | 111 |
112 | ); 113 | }; 114 | 115 | export const StandardTooltipContent: React.FC<{ 116 | task: Task; 117 | fontSize: string; 118 | fontFamily: string; 119 | }> = ({ task, fontSize, fontFamily }) => { 120 | const style = { 121 | fontSize, 122 | fontFamily, 123 | }; 124 | return ( 125 |
126 | {`${ 127 | task.name 128 | }: ${task.start.getDate()}-${ 129 | task.start.getMonth() + 1 130 | }-${task.start.getFullYear()} - ${task.end.getDate()}-${ 131 | task.end.getMonth() + 1 132 | }-${task.end.getFullYear()}`} 133 | {task.end.getTime() - task.start.getTime() !== 0 && ( 134 |

{`Duration: ${~~( 135 | (task.end.getTime() - task.start.getTime()) / 136 | (1000 * 60 * 60 * 24) 137 | )} day(s)`}

138 | )} 139 | 140 |

141 | {!!task.progress && `Progress: ${task.progress} %`} 142 |

143 |
144 | ); 145 | }; 146 | -------------------------------------------------------------------------------- /src/components/other/vertical-scroll.module.css: -------------------------------------------------------------------------------- 1 | .scroll { 2 | overflow: hidden auto; 3 | width: 1rem; 4 | flex-shrink: 0; 5 | /*firefox*/ 6 | scrollbar-width: thin; 7 | } 8 | .scroll::-webkit-scrollbar { 9 | width: 1.1rem; 10 | height: 1.1rem; 11 | } 12 | .scroll::-webkit-scrollbar-corner { 13 | background: transparent; 14 | } 15 | .scroll::-webkit-scrollbar-thumb { 16 | border: 6px solid transparent; 17 | background: rgba(0, 0, 0, 0.2); 18 | background: var(--palette-black-alpha-20, rgba(0, 0, 0, 0.2)); 19 | border-radius: 10px; 20 | background-clip: padding-box; 21 | } 22 | .scroll::-webkit-scrollbar-thumb:hover { 23 | border: 4px solid transparent; 24 | background: rgba(0, 0, 0, 0.3); 25 | background: var(--palette-black-alpha-30, rgba(0, 0, 0, 0.3)); 26 | background-clip: padding-box; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/other/vertical-scroll.tsx: -------------------------------------------------------------------------------- 1 | import React, { SyntheticEvent, useRef, useEffect } from "react"; 2 | import styles from "./vertical-scroll.module.css"; 3 | 4 | export const VerticalScroll: React.FC<{ 5 | scroll: number; 6 | ganttHeight: number; 7 | ganttFullHeight: number; 8 | headerHeight: number; 9 | rtl: boolean; 10 | onScroll: (event: SyntheticEvent) => void; 11 | }> = ({ 12 | scroll, 13 | ganttHeight, 14 | ganttFullHeight, 15 | headerHeight, 16 | rtl, 17 | onScroll, 18 | }) => { 19 | const scrollRef = useRef(null); 20 | 21 | useEffect(() => { 22 | if (scrollRef.current) { 23 | scrollRef.current.scrollTop = scroll; 24 | } 25 | }, [scroll]); 26 | 27 | return ( 28 |
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/task-item/bar/bar-date-handle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./bar.module.css"; 3 | 4 | type BarDateHandleProps = { 5 | x: number; 6 | y: number; 7 | width: number; 8 | height: number; 9 | barCornerRadius: number; 10 | onMouseDown: (event: React.MouseEvent) => void; 11 | }; 12 | export const BarDateHandle: React.FC = ({ 13 | x, 14 | y, 15 | width, 16 | height, 17 | barCornerRadius, 18 | onMouseDown, 19 | }) => { 20 | return ( 21 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/task-item/bar/bar-display.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import style from "./bar.module.css"; 3 | 4 | type BarDisplayProps = { 5 | x: number; 6 | y: number; 7 | width: number; 8 | height: number; 9 | isSelected: boolean; 10 | /* progress start point */ 11 | progressX: number; 12 | progressWidth: number; 13 | barCornerRadius: number; 14 | styles: { 15 | backgroundColor: string; 16 | backgroundSelectedColor: string; 17 | progressColor: string; 18 | progressSelectedColor: string; 19 | }; 20 | onMouseDown: (event: React.MouseEvent) => void; 21 | }; 22 | export const BarDisplay: React.FC = ({ 23 | x, 24 | y, 25 | width, 26 | height, 27 | isSelected, 28 | progressX, 29 | progressWidth, 30 | barCornerRadius, 31 | styles, 32 | onMouseDown, 33 | }) => { 34 | const getProcessColor = () => { 35 | return isSelected ? styles.progressSelectedColor : styles.progressColor; 36 | }; 37 | 38 | const getBarColor = () => { 39 | return isSelected ? styles.backgroundSelectedColor : styles.backgroundColor; 40 | }; 41 | 42 | return ( 43 | 44 | 54 | 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/components/task-item/bar/bar-progress-handle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./bar.module.css"; 3 | 4 | type BarProgressHandleProps = { 5 | progressPoint: string; 6 | onMouseDown: (event: React.MouseEvent) => void; 7 | }; 8 | export const BarProgressHandle: React.FC = ({ 9 | progressPoint, 10 | onMouseDown, 11 | }) => { 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/task-item/bar/bar-small.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getProgressPoint } from "../../../helpers/bar-helper"; 3 | import { BarDisplay } from "./bar-display"; 4 | import { BarProgressHandle } from "./bar-progress-handle"; 5 | import { TaskItemProps } from "../task-item"; 6 | import styles from "./bar.module.css"; 7 | 8 | export const BarSmall: React.FC = ({ 9 | task, 10 | isProgressChangeable, 11 | isDateChangeable, 12 | onEventStart, 13 | isSelected, 14 | }) => { 15 | const progressPoint = getProgressPoint( 16 | task.progressWidth + task.x1, 17 | task.y, 18 | task.height 19 | ); 20 | return ( 21 | 22 | { 33 | isDateChangeable && onEventStart("move", task, e); 34 | }} 35 | /> 36 | 37 | {isProgressChangeable && ( 38 | { 41 | onEventStart("progress", task, e); 42 | }} 43 | /> 44 | )} 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/task-item/bar/bar.module.css: -------------------------------------------------------------------------------- 1 | .barWrapper { 2 | cursor: pointer; 3 | outline: none; 4 | } 5 | 6 | .barWrapper:hover .barHandle { 7 | visibility: visible; 8 | opacity: 1; 9 | } 10 | 11 | .barHandle { 12 | fill: #ddd; 13 | cursor: ew-resize; 14 | opacity: 0; 15 | visibility: hidden; 16 | } 17 | 18 | .barBackground { 19 | user-select: none; 20 | stroke-width: 0; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/task-item/bar/bar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getProgressPoint } from "../../../helpers/bar-helper"; 3 | import { BarDisplay } from "./bar-display"; 4 | import { BarDateHandle } from "./bar-date-handle"; 5 | import { BarProgressHandle } from "./bar-progress-handle"; 6 | import { TaskItemProps } from "../task-item"; 7 | import styles from "./bar.module.css"; 8 | 9 | export const Bar: React.FC = ({ 10 | task, 11 | isProgressChangeable, 12 | isDateChangeable, 13 | rtl, 14 | onEventStart, 15 | isSelected, 16 | }) => { 17 | const progressPoint = getProgressPoint( 18 | +!rtl * task.progressWidth + task.progressX, 19 | task.y, 20 | task.height 21 | ); 22 | const handleHeight = task.height - 2; 23 | return ( 24 | 25 | { 36 | isDateChangeable && onEventStart("move", task, e); 37 | }} 38 | /> 39 | 40 | {isDateChangeable && ( 41 | 42 | {/* left */} 43 | { 50 | onEventStart("start", task, e); 51 | }} 52 | /> 53 | {/* right */} 54 | { 61 | onEventStart("end", task, e); 62 | }} 63 | /> 64 | 65 | )} 66 | {isProgressChangeable && ( 67 | { 70 | onEventStart("progress", task, e); 71 | }} 72 | /> 73 | )} 74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/task-item/milestone/milestone.module.css: -------------------------------------------------------------------------------- 1 | .milestoneWrapper { 2 | cursor: pointer; 3 | outline: none; 4 | } 5 | 6 | .milestoneBackground { 7 | user-select: none; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/task-item/milestone/milestone.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TaskItemProps } from "../task-item"; 3 | import styles from "./milestone.module.css"; 4 | 5 | export const Milestone: React.FC = ({ 6 | task, 7 | isDateChangeable, 8 | onEventStart, 9 | isSelected, 10 | }) => { 11 | const transform = `rotate(45 ${task.x1 + task.height * 0.356} 12 | ${task.y + task.height * 0.85})`; 13 | const getBarColor = () => { 14 | return isSelected 15 | ? task.styles.backgroundSelectedColor 16 | : task.styles.backgroundColor; 17 | }; 18 | 19 | return ( 20 | 21 | { 32 | isDateChangeable && onEventStart("move", task, e); 33 | }} 34 | /> 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/task-item/project/project.module.css: -------------------------------------------------------------------------------- 1 | .projectWrapper { 2 | cursor: pointer; 3 | outline: none; 4 | } 5 | 6 | .projectBackground { 7 | user-select: none; 8 | opacity: 0.6; 9 | } 10 | 11 | .projectTop { 12 | user-select: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/task-item/project/project.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TaskItemProps } from "../task-item"; 3 | import styles from "./project.module.css"; 4 | 5 | export const Project: React.FC = ({ task, isSelected }) => { 6 | const barColor = isSelected 7 | ? task.styles.backgroundSelectedColor 8 | : task.styles.backgroundColor; 9 | const processColor = isSelected 10 | ? task.styles.progressSelectedColor 11 | : task.styles.progressColor; 12 | const projectWith = task.x2 - task.x1; 13 | 14 | const projectLeftTriangle = [ 15 | task.x1, 16 | task.y + task.height / 2 - 1, 17 | task.x1, 18 | task.y + task.height, 19 | task.x1 + 15, 20 | task.y + task.height / 2 - 1, 21 | ].join(","); 22 | const projectRightTriangle = [ 23 | task.x2, 24 | task.y + task.height / 2 - 1, 25 | task.x2, 26 | task.y + task.height, 27 | task.x2 - 15, 28 | task.y + task.height / 2 - 1, 29 | ].join(","); 30 | 31 | return ( 32 | 33 | 43 | 52 | 62 | 67 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/task-item/task-item.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { BarTask } from "../../types/bar-task"; 3 | import { GanttContentMoveAction } from "../../types/gantt-task-actions"; 4 | import { Bar } from "./bar/bar"; 5 | import { BarSmall } from "./bar/bar-small"; 6 | import { Milestone } from "./milestone/milestone"; 7 | import { Project } from "./project/project"; 8 | import style from "./task-list.module.css"; 9 | 10 | export type TaskItemProps = { 11 | task: BarTask; 12 | arrowIndent: number; 13 | taskHeight: number; 14 | isProgressChangeable: boolean; 15 | isDateChangeable: boolean; 16 | isDelete: boolean; 17 | isSelected: boolean; 18 | rtl: boolean; 19 | onEventStart: ( 20 | action: GanttContentMoveAction, 21 | selectedTask: BarTask, 22 | event?: React.MouseEvent | React.KeyboardEvent 23 | ) => any; 24 | }; 25 | 26 | export const TaskItem: React.FC = props => { 27 | const { 28 | task, 29 | arrowIndent, 30 | isDelete, 31 | taskHeight, 32 | isSelected, 33 | rtl, 34 | onEventStart, 35 | } = { 36 | ...props, 37 | }; 38 | const textRef = useRef(null); 39 | const [taskItem, setTaskItem] = useState(
); 40 | const [isTextInside, setIsTextInside] = useState(true); 41 | 42 | useEffect(() => { 43 | switch (task.typeInternal) { 44 | case "milestone": 45 | setTaskItem(); 46 | break; 47 | case "project": 48 | setTaskItem(); 49 | break; 50 | case "smalltask": 51 | setTaskItem(); 52 | break; 53 | default: 54 | setTaskItem(); 55 | break; 56 | } 57 | }, [task, isSelected]); 58 | 59 | useEffect(() => { 60 | if (textRef.current) { 61 | setIsTextInside(textRef.current.getBBox().width < task.x2 - task.x1); 62 | } 63 | }, [textRef, task]); 64 | 65 | const getX = () => { 66 | const width = task.x2 - task.x1; 67 | const hasChild = task.barChildren.length > 0; 68 | if (isTextInside) { 69 | return task.x1 + width * 0.5; 70 | } 71 | if (rtl && textRef.current) { 72 | return ( 73 | task.x1 - 74 | textRef.current.getBBox().width - 75 | arrowIndent * +hasChild - 76 | arrowIndent * 0.2 77 | ); 78 | } else { 79 | return task.x1 + width + arrowIndent * +hasChild + arrowIndent * 0.2; 80 | } 81 | }; 82 | 83 | return ( 84 | { 86 | switch (e.key) { 87 | case "Delete": { 88 | if (isDelete) onEventStart("delete", task, e); 89 | break; 90 | } 91 | } 92 | e.stopPropagation(); 93 | }} 94 | onMouseEnter={e => { 95 | onEventStart("mouseenter", task, e); 96 | }} 97 | onMouseLeave={e => { 98 | onEventStart("mouseleave", task, e); 99 | }} 100 | onDoubleClick={e => { 101 | onEventStart("dblclick", task, e); 102 | }} 103 | onClick={e => { 104 | onEventStart("click", task, e); 105 | }} 106 | onFocus={() => { 107 | onEventStart("select", task); 108 | }} 109 | > 110 | {taskItem} 111 | 121 | {task.name} 122 | 123 | 124 | ); 125 | }; 126 | -------------------------------------------------------------------------------- /src/components/task-item/task-list.module.css: -------------------------------------------------------------------------------- 1 | .barLabel { 2 | fill: #fff; 3 | text-anchor: middle; 4 | font-weight: lighter; 5 | dominant-baseline: central; 6 | -webkit-touch-callout: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | pointer-events: none; 12 | } 13 | 14 | .barLabelOutside { 15 | fill: #555; 16 | text-anchor: start; 17 | -webkit-touch-callout: none; 18 | -webkit-user-select: none; 19 | -moz-user-select: none; 20 | -ms-user-select: none; 21 | user-select: none; 22 | pointer-events: none; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/task-list/task-list-header.module.css: -------------------------------------------------------------------------------- 1 | .ganttTable { 2 | display: table; 3 | border-bottom: #e6e4e4 1px solid; 4 | border-top: #e6e4e4 1px solid; 5 | border-left: #e6e4e4 1px solid; 6 | } 7 | 8 | .ganttTable_Header { 9 | display: table-row; 10 | list-style: none; 11 | } 12 | 13 | .ganttTable_HeaderSeparator { 14 | border-right: 1px solid rgb(196, 196, 196); 15 | opacity: 1; 16 | margin-left: -2px; 17 | } 18 | 19 | .ganttTable_HeaderItem { 20 | display: table-cell; 21 | vertical-align: -webkit-baseline-middle; 22 | vertical-align: middle; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/task-list/task-list-header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./task-list-header.module.css"; 3 | 4 | export const TaskListHeaderDefault: React.FC<{ 5 | headerHeight: number; 6 | rowWidth: string; 7 | fontFamily: string; 8 | fontSize: string; 9 | }> = ({ headerHeight, fontFamily, fontSize, rowWidth }) => { 10 | return ( 11 |
18 |
24 |
30 |  Name 31 |
32 |
39 |
45 |  From 46 |
47 |
54 |
60 |  To 61 |
62 |
63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/components/task-list/task-list-table.module.css: -------------------------------------------------------------------------------- 1 | .taskListWrapper { 2 | display: table; 3 | border-bottom: #e6e4e4 1px solid; 4 | border-left: #e6e4e4 1px solid; 5 | } 6 | 7 | .taskListTableRow { 8 | display: table-row; 9 | text-overflow: ellipsis; 10 | } 11 | 12 | .taskListTableRow:nth-of-type(even) { 13 | background-color: #f5f5f5; 14 | } 15 | 16 | .taskListCell { 17 | display: table-cell; 18 | vertical-align: middle; 19 | white-space: nowrap; 20 | overflow: hidden; 21 | text-overflow: ellipsis; 22 | } 23 | .taskListNameWrapper { 24 | display: flex; 25 | } 26 | 27 | .taskListExpander { 28 | color: rgb(86 86 86); 29 | font-size: 0.6rem; 30 | padding: 0.15rem 0.2rem 0rem 0.2rem; 31 | user-select: none; 32 | cursor: pointer; 33 | } 34 | .taskListEmptyExpander { 35 | font-size: 0.6rem; 36 | padding-left: 1rem; 37 | user-select: none; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/task-list/task-list-table.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import styles from "./task-list-table.module.css"; 3 | import { Task } from "../../types/public-types"; 4 | 5 | const localeDateStringCache = {}; 6 | const toLocaleDateStringFactory = 7 | (locale: string) => 8 | (date: Date, dateTimeOptions: Intl.DateTimeFormatOptions) => { 9 | const key = date.toString(); 10 | let lds = localeDateStringCache[key]; 11 | if (!lds) { 12 | lds = date.toLocaleDateString(locale, dateTimeOptions); 13 | localeDateStringCache[key] = lds; 14 | } 15 | return lds; 16 | }; 17 | const dateTimeOptions: Intl.DateTimeFormatOptions = { 18 | weekday: "short", 19 | year: "numeric", 20 | month: "long", 21 | day: "numeric", 22 | }; 23 | 24 | export const TaskListTableDefault: React.FC<{ 25 | rowHeight: number; 26 | rowWidth: string; 27 | fontFamily: string; 28 | fontSize: string; 29 | locale: string; 30 | tasks: Task[]; 31 | selectedTaskId: string; 32 | setSelectedTask: (taskId: string) => void; 33 | onExpanderClick: (task: Task) => void; 34 | }> = ({ 35 | rowHeight, 36 | rowWidth, 37 | tasks, 38 | fontFamily, 39 | fontSize, 40 | locale, 41 | onExpanderClick, 42 | }) => { 43 | const toLocaleDateString = useMemo( 44 | () => toLocaleDateStringFactory(locale), 45 | [locale] 46 | ); 47 | 48 | return ( 49 |
56 | {tasks.map(t => { 57 | let expanderSymbol = ""; 58 | if (t.hideChildren === false) { 59 | expanderSymbol = "▼"; 60 | } else if (t.hideChildren === true) { 61 | expanderSymbol = "▶"; 62 | } 63 | 64 | return ( 65 |
70 |
78 |
79 |
onExpanderClick(t)} 86 | > 87 | {expanderSymbol} 88 |
89 |
{t.name}
90 |
91 |
92 |
99 |  {toLocaleDateString(t.start, dateTimeOptions)} 100 |
101 |
108 |  {toLocaleDateString(t.end, dateTimeOptions)} 109 |
110 |
111 | ); 112 | })} 113 |
114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /src/components/task-list/task-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { BarTask } from "../../types/bar-task"; 3 | import { Task } from "../../types/public-types"; 4 | 5 | export type TaskListProps = { 6 | headerHeight: number; 7 | rowWidth: string; 8 | fontFamily: string; 9 | fontSize: string; 10 | rowHeight: number; 11 | ganttHeight: number; 12 | scrollY: number; 13 | locale: string; 14 | tasks: Task[]; 15 | taskListRef: React.RefObject; 16 | horizontalContainerClass?: string; 17 | selectedTask: BarTask | undefined; 18 | setSelectedTask: (task: string) => void; 19 | onExpanderClick: (task: Task) => void; 20 | TaskListHeader: React.FC<{ 21 | headerHeight: number; 22 | rowWidth: string; 23 | fontFamily: string; 24 | fontSize: string; 25 | }>; 26 | TaskListTable: React.FC<{ 27 | rowHeight: number; 28 | rowWidth: string; 29 | fontFamily: string; 30 | fontSize: string; 31 | locale: string; 32 | tasks: Task[]; 33 | selectedTaskId: string; 34 | setSelectedTask: (taskId: string) => void; 35 | onExpanderClick: (task: Task) => void; 36 | }>; 37 | }; 38 | 39 | export const TaskList: React.FC = ({ 40 | headerHeight, 41 | fontFamily, 42 | fontSize, 43 | rowWidth, 44 | rowHeight, 45 | scrollY, 46 | tasks, 47 | selectedTask, 48 | setSelectedTask, 49 | onExpanderClick, 50 | locale, 51 | ganttHeight, 52 | taskListRef, 53 | horizontalContainerClass, 54 | TaskListHeader, 55 | TaskListTable, 56 | }) => { 57 | const horizontalContainerRef = useRef(null); 58 | useEffect(() => { 59 | if (horizontalContainerRef.current) { 60 | horizontalContainerRef.current.scrollTop = scrollY; 61 | } 62 | }, [scrollY]); 63 | 64 | const headerProps = { 65 | headerHeight, 66 | fontFamily, 67 | fontSize, 68 | rowWidth, 69 | }; 70 | const selectedTaskId = selectedTask ? selectedTask.id : ""; 71 | const tableProps = { 72 | rowHeight, 73 | rowWidth, 74 | fontFamily, 75 | fontSize, 76 | tasks, 77 | locale, 78 | selectedTaskId: selectedTaskId, 79 | setSelectedTask, 80 | onExpanderClick, 81 | }; 82 | 83 | return ( 84 |
85 | 86 |
91 | 92 |
93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/helpers/bar-helper.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "../types/public-types"; 2 | import { BarTask, TaskTypeInternal } from "../types/bar-task"; 3 | import { BarMoveAction } from "../types/gantt-task-actions"; 4 | 5 | export const convertToBarTasks = ( 6 | tasks: Task[], 7 | dates: Date[], 8 | columnWidth: number, 9 | rowHeight: number, 10 | taskHeight: number, 11 | barCornerRadius: number, 12 | handleWidth: number, 13 | rtl: boolean, 14 | barProgressColor: string, 15 | barProgressSelectedColor: string, 16 | barBackgroundColor: string, 17 | barBackgroundSelectedColor: string, 18 | projectProgressColor: string, 19 | projectProgressSelectedColor: string, 20 | projectBackgroundColor: string, 21 | projectBackgroundSelectedColor: string, 22 | milestoneBackgroundColor: string, 23 | milestoneBackgroundSelectedColor: string 24 | ) => { 25 | let barTasks = tasks.map((t, i) => { 26 | return convertToBarTask( 27 | t, 28 | i, 29 | dates, 30 | columnWidth, 31 | rowHeight, 32 | taskHeight, 33 | barCornerRadius, 34 | handleWidth, 35 | rtl, 36 | barProgressColor, 37 | barProgressSelectedColor, 38 | barBackgroundColor, 39 | barBackgroundSelectedColor, 40 | projectProgressColor, 41 | projectProgressSelectedColor, 42 | projectBackgroundColor, 43 | projectBackgroundSelectedColor, 44 | milestoneBackgroundColor, 45 | milestoneBackgroundSelectedColor 46 | ); 47 | }); 48 | 49 | // set dependencies 50 | barTasks = barTasks.map(task => { 51 | const dependencies = task.dependencies || []; 52 | for (let j = 0; j < dependencies.length; j++) { 53 | const dependence = barTasks.findIndex( 54 | value => value.id === dependencies[j] 55 | ); 56 | if (dependence !== -1) barTasks[dependence].barChildren.push(task); 57 | } 58 | return task; 59 | }); 60 | 61 | return barTasks; 62 | }; 63 | 64 | const convertToBarTask = ( 65 | task: Task, 66 | index: number, 67 | dates: Date[], 68 | columnWidth: number, 69 | rowHeight: number, 70 | taskHeight: number, 71 | barCornerRadius: number, 72 | handleWidth: number, 73 | rtl: boolean, 74 | barProgressColor: string, 75 | barProgressSelectedColor: string, 76 | barBackgroundColor: string, 77 | barBackgroundSelectedColor: string, 78 | projectProgressColor: string, 79 | projectProgressSelectedColor: string, 80 | projectBackgroundColor: string, 81 | projectBackgroundSelectedColor: string, 82 | milestoneBackgroundColor: string, 83 | milestoneBackgroundSelectedColor: string 84 | ): BarTask => { 85 | let barTask: BarTask; 86 | switch (task.type) { 87 | case "milestone": 88 | barTask = convertToMilestone( 89 | task, 90 | index, 91 | dates, 92 | columnWidth, 93 | rowHeight, 94 | taskHeight, 95 | barCornerRadius, 96 | handleWidth, 97 | milestoneBackgroundColor, 98 | milestoneBackgroundSelectedColor 99 | ); 100 | break; 101 | case "project": 102 | barTask = convertToBar( 103 | task, 104 | index, 105 | dates, 106 | columnWidth, 107 | rowHeight, 108 | taskHeight, 109 | barCornerRadius, 110 | handleWidth, 111 | rtl, 112 | projectProgressColor, 113 | projectProgressSelectedColor, 114 | projectBackgroundColor, 115 | projectBackgroundSelectedColor 116 | ); 117 | break; 118 | default: 119 | barTask = convertToBar( 120 | task, 121 | index, 122 | dates, 123 | columnWidth, 124 | rowHeight, 125 | taskHeight, 126 | barCornerRadius, 127 | handleWidth, 128 | rtl, 129 | barProgressColor, 130 | barProgressSelectedColor, 131 | barBackgroundColor, 132 | barBackgroundSelectedColor 133 | ); 134 | break; 135 | } 136 | return barTask; 137 | }; 138 | 139 | const convertToBar = ( 140 | task: Task, 141 | index: number, 142 | dates: Date[], 143 | columnWidth: number, 144 | rowHeight: number, 145 | taskHeight: number, 146 | barCornerRadius: number, 147 | handleWidth: number, 148 | rtl: boolean, 149 | barProgressColor: string, 150 | barProgressSelectedColor: string, 151 | barBackgroundColor: string, 152 | barBackgroundSelectedColor: string 153 | ): BarTask => { 154 | let x1: number; 155 | let x2: number; 156 | if (rtl) { 157 | x2 = taskXCoordinateRTL(task.start, dates, columnWidth); 158 | x1 = taskXCoordinateRTL(task.end, dates, columnWidth); 159 | } else { 160 | x1 = taskXCoordinate(task.start, dates, columnWidth); 161 | x2 = taskXCoordinate(task.end, dates, columnWidth); 162 | } 163 | let typeInternal: TaskTypeInternal = task.type; 164 | if (typeInternal === "task" && x2 - x1 < handleWidth * 2) { 165 | typeInternal = "smalltask"; 166 | x2 = x1 + handleWidth * 2; 167 | } 168 | 169 | const [progressWidth, progressX] = progressWithByParams( 170 | x1, 171 | x2, 172 | task.progress, 173 | rtl 174 | ); 175 | const y = taskYCoordinate(index, rowHeight, taskHeight); 176 | const hideChildren = task.type === "project" ? task.hideChildren : undefined; 177 | 178 | const styles = { 179 | backgroundColor: barBackgroundColor, 180 | backgroundSelectedColor: barBackgroundSelectedColor, 181 | progressColor: barProgressColor, 182 | progressSelectedColor: barProgressSelectedColor, 183 | ...task.styles, 184 | }; 185 | return { 186 | ...task, 187 | typeInternal, 188 | x1, 189 | x2, 190 | y, 191 | index, 192 | progressX, 193 | progressWidth, 194 | barCornerRadius, 195 | handleWidth, 196 | hideChildren, 197 | height: taskHeight, 198 | barChildren: [], 199 | styles, 200 | }; 201 | }; 202 | 203 | const convertToMilestone = ( 204 | task: Task, 205 | index: number, 206 | dates: Date[], 207 | columnWidth: number, 208 | rowHeight: number, 209 | taskHeight: number, 210 | barCornerRadius: number, 211 | handleWidth: number, 212 | milestoneBackgroundColor: string, 213 | milestoneBackgroundSelectedColor: string 214 | ): BarTask => { 215 | const x = taskXCoordinate(task.start, dates, columnWidth); 216 | const y = taskYCoordinate(index, rowHeight, taskHeight); 217 | 218 | const x1 = x - taskHeight * 0.5; 219 | const x2 = x + taskHeight * 0.5; 220 | 221 | const rotatedHeight = taskHeight / 1.414; 222 | const styles = { 223 | backgroundColor: milestoneBackgroundColor, 224 | backgroundSelectedColor: milestoneBackgroundSelectedColor, 225 | progressColor: "", 226 | progressSelectedColor: "", 227 | ...task.styles, 228 | }; 229 | return { 230 | ...task, 231 | end: task.start, 232 | x1, 233 | x2, 234 | y, 235 | index, 236 | progressX: 0, 237 | progressWidth: 0, 238 | barCornerRadius, 239 | handleWidth, 240 | typeInternal: task.type, 241 | progress: 0, 242 | height: rotatedHeight, 243 | hideChildren: undefined, 244 | barChildren: [], 245 | styles, 246 | }; 247 | }; 248 | 249 | const taskXCoordinate = (xDate: Date, dates: Date[], columnWidth: number) => { 250 | const index = dates.findIndex(d => d.getTime() >= xDate.getTime()) - 1; 251 | 252 | const remainderMillis = xDate.getTime() - dates[index].getTime(); 253 | const percentOfInterval = 254 | remainderMillis / (dates[index + 1].getTime() - dates[index].getTime()); 255 | const x = index * columnWidth + percentOfInterval * columnWidth; 256 | return x; 257 | }; 258 | const taskXCoordinateRTL = ( 259 | xDate: Date, 260 | dates: Date[], 261 | columnWidth: number 262 | ) => { 263 | let x = taskXCoordinate(xDate, dates, columnWidth); 264 | x += columnWidth; 265 | return x; 266 | }; 267 | const taskYCoordinate = ( 268 | index: number, 269 | rowHeight: number, 270 | taskHeight: number 271 | ) => { 272 | const y = index * rowHeight + (rowHeight - taskHeight) / 2; 273 | return y; 274 | }; 275 | 276 | export const progressWithByParams = ( 277 | taskX1: number, 278 | taskX2: number, 279 | progress: number, 280 | rtl: boolean 281 | ) => { 282 | const progressWidth = (taskX2 - taskX1) * progress * 0.01; 283 | let progressX: number; 284 | if (rtl) { 285 | progressX = taskX2 - progressWidth; 286 | } else { 287 | progressX = taskX1; 288 | } 289 | return [progressWidth, progressX]; 290 | }; 291 | 292 | export const progressByProgressWidth = ( 293 | progressWidth: number, 294 | barTask: BarTask 295 | ) => { 296 | const barWidth = barTask.x2 - barTask.x1; 297 | const progressPercent = Math.round((progressWidth * 100) / barWidth); 298 | if (progressPercent >= 100) return 100; 299 | else if (progressPercent <= 0) return 0; 300 | else return progressPercent; 301 | }; 302 | 303 | const progressByX = (x: number, task: BarTask) => { 304 | if (x >= task.x2) return 100; 305 | else if (x <= task.x1) return 0; 306 | else { 307 | const barWidth = task.x2 - task.x1; 308 | const progressPercent = Math.round(((x - task.x1) * 100) / barWidth); 309 | return progressPercent; 310 | } 311 | }; 312 | const progressByXRTL = (x: number, task: BarTask) => { 313 | if (x >= task.x2) return 0; 314 | else if (x <= task.x1) return 100; 315 | else { 316 | const barWidth = task.x2 - task.x1; 317 | const progressPercent = Math.round(((task.x2 - x) * 100) / barWidth); 318 | return progressPercent; 319 | } 320 | }; 321 | 322 | export const getProgressPoint = ( 323 | progressX: number, 324 | taskY: number, 325 | taskHeight: number 326 | ) => { 327 | const point = [ 328 | progressX - 5, 329 | taskY + taskHeight, 330 | progressX + 5, 331 | taskY + taskHeight, 332 | progressX, 333 | taskY + taskHeight - 8.66, 334 | ]; 335 | return point.join(","); 336 | }; 337 | 338 | const startByX = (x: number, xStep: number, task: BarTask) => { 339 | if (x >= task.x2 - task.handleWidth * 2) { 340 | x = task.x2 - task.handleWidth * 2; 341 | } 342 | const steps = Math.round((x - task.x1) / xStep); 343 | const additionalXValue = steps * xStep; 344 | const newX = task.x1 + additionalXValue; 345 | return newX; 346 | }; 347 | 348 | const endByX = (x: number, xStep: number, task: BarTask) => { 349 | if (x <= task.x1 + task.handleWidth * 2) { 350 | x = task.x1 + task.handleWidth * 2; 351 | } 352 | const steps = Math.round((x - task.x2) / xStep); 353 | const additionalXValue = steps * xStep; 354 | const newX = task.x2 + additionalXValue; 355 | return newX; 356 | }; 357 | 358 | const moveByX = (x: number, xStep: number, task: BarTask) => { 359 | const steps = Math.round((x - task.x1) / xStep); 360 | const additionalXValue = steps * xStep; 361 | const newX1 = task.x1 + additionalXValue; 362 | const newX2 = newX1 + task.x2 - task.x1; 363 | return [newX1, newX2]; 364 | }; 365 | 366 | const dateByX = ( 367 | x: number, 368 | taskX: number, 369 | taskDate: Date, 370 | xStep: number, 371 | timeStep: number 372 | ) => { 373 | let newDate = new Date(((x - taskX) / xStep) * timeStep + taskDate.getTime()); 374 | newDate = new Date( 375 | newDate.getTime() + 376 | (newDate.getTimezoneOffset() - taskDate.getTimezoneOffset()) * 60000 377 | ); 378 | return newDate; 379 | }; 380 | 381 | /** 382 | * Method handles event in real time(mousemove) and on finish(mouseup) 383 | */ 384 | export const handleTaskBySVGMouseEvent = ( 385 | svgX: number, 386 | action: BarMoveAction, 387 | selectedTask: BarTask, 388 | xStep: number, 389 | timeStep: number, 390 | initEventX1Delta: number, 391 | rtl: boolean 392 | ): { isChanged: boolean; changedTask: BarTask } => { 393 | let result: { isChanged: boolean; changedTask: BarTask }; 394 | switch (selectedTask.type) { 395 | case "milestone": 396 | result = handleTaskBySVGMouseEventForMilestone( 397 | svgX, 398 | action, 399 | selectedTask, 400 | xStep, 401 | timeStep, 402 | initEventX1Delta 403 | ); 404 | break; 405 | default: 406 | result = handleTaskBySVGMouseEventForBar( 407 | svgX, 408 | action, 409 | selectedTask, 410 | xStep, 411 | timeStep, 412 | initEventX1Delta, 413 | rtl 414 | ); 415 | break; 416 | } 417 | return result; 418 | }; 419 | 420 | const handleTaskBySVGMouseEventForBar = ( 421 | svgX: number, 422 | action: BarMoveAction, 423 | selectedTask: BarTask, 424 | xStep: number, 425 | timeStep: number, 426 | initEventX1Delta: number, 427 | rtl: boolean 428 | ): { isChanged: boolean; changedTask: BarTask } => { 429 | const changedTask: BarTask = { ...selectedTask }; 430 | let isChanged = false; 431 | switch (action) { 432 | case "progress": 433 | if (rtl) { 434 | changedTask.progress = progressByXRTL(svgX, selectedTask); 435 | } else { 436 | changedTask.progress = progressByX(svgX, selectedTask); 437 | } 438 | isChanged = changedTask.progress !== selectedTask.progress; 439 | if (isChanged) { 440 | const [progressWidth, progressX] = progressWithByParams( 441 | changedTask.x1, 442 | changedTask.x2, 443 | changedTask.progress, 444 | rtl 445 | ); 446 | changedTask.progressWidth = progressWidth; 447 | changedTask.progressX = progressX; 448 | } 449 | break; 450 | case "start": { 451 | const newX1 = startByX(svgX, xStep, selectedTask); 452 | changedTask.x1 = newX1; 453 | isChanged = changedTask.x1 !== selectedTask.x1; 454 | if (isChanged) { 455 | if (rtl) { 456 | changedTask.end = dateByX( 457 | newX1, 458 | selectedTask.x1, 459 | selectedTask.end, 460 | xStep, 461 | timeStep 462 | ); 463 | } else { 464 | changedTask.start = dateByX( 465 | newX1, 466 | selectedTask.x1, 467 | selectedTask.start, 468 | xStep, 469 | timeStep 470 | ); 471 | } 472 | const [progressWidth, progressX] = progressWithByParams( 473 | changedTask.x1, 474 | changedTask.x2, 475 | changedTask.progress, 476 | rtl 477 | ); 478 | changedTask.progressWidth = progressWidth; 479 | changedTask.progressX = progressX; 480 | } 481 | break; 482 | } 483 | case "end": { 484 | const newX2 = endByX(svgX, xStep, selectedTask); 485 | changedTask.x2 = newX2; 486 | isChanged = changedTask.x2 !== selectedTask.x2; 487 | if (isChanged) { 488 | if (rtl) { 489 | changedTask.start = dateByX( 490 | newX2, 491 | selectedTask.x2, 492 | selectedTask.start, 493 | xStep, 494 | timeStep 495 | ); 496 | } else { 497 | changedTask.end = dateByX( 498 | newX2, 499 | selectedTask.x2, 500 | selectedTask.end, 501 | xStep, 502 | timeStep 503 | ); 504 | } 505 | const [progressWidth, progressX] = progressWithByParams( 506 | changedTask.x1, 507 | changedTask.x2, 508 | changedTask.progress, 509 | rtl 510 | ); 511 | changedTask.progressWidth = progressWidth; 512 | changedTask.progressX = progressX; 513 | } 514 | break; 515 | } 516 | case "move": { 517 | const [newMoveX1, newMoveX2] = moveByX( 518 | svgX - initEventX1Delta, 519 | xStep, 520 | selectedTask 521 | ); 522 | isChanged = newMoveX1 !== selectedTask.x1; 523 | if (isChanged) { 524 | changedTask.start = dateByX( 525 | newMoveX1, 526 | selectedTask.x1, 527 | selectedTask.start, 528 | xStep, 529 | timeStep 530 | ); 531 | changedTask.end = dateByX( 532 | newMoveX2, 533 | selectedTask.x2, 534 | selectedTask.end, 535 | xStep, 536 | timeStep 537 | ); 538 | changedTask.x1 = newMoveX1; 539 | changedTask.x2 = newMoveX2; 540 | const [progressWidth, progressX] = progressWithByParams( 541 | changedTask.x1, 542 | changedTask.x2, 543 | changedTask.progress, 544 | rtl 545 | ); 546 | changedTask.progressWidth = progressWidth; 547 | changedTask.progressX = progressX; 548 | } 549 | break; 550 | } 551 | } 552 | return { isChanged, changedTask }; 553 | }; 554 | 555 | const handleTaskBySVGMouseEventForMilestone = ( 556 | svgX: number, 557 | action: BarMoveAction, 558 | selectedTask: BarTask, 559 | xStep: number, 560 | timeStep: number, 561 | initEventX1Delta: number 562 | ): { isChanged: boolean; changedTask: BarTask } => { 563 | const changedTask: BarTask = { ...selectedTask }; 564 | let isChanged = false; 565 | switch (action) { 566 | case "move": { 567 | const [newMoveX1, newMoveX2] = moveByX( 568 | svgX - initEventX1Delta, 569 | xStep, 570 | selectedTask 571 | ); 572 | isChanged = newMoveX1 !== selectedTask.x1; 573 | if (isChanged) { 574 | changedTask.start = dateByX( 575 | newMoveX1, 576 | selectedTask.x1, 577 | selectedTask.start, 578 | xStep, 579 | timeStep 580 | ); 581 | changedTask.end = changedTask.start; 582 | changedTask.x1 = newMoveX1; 583 | changedTask.x2 = newMoveX2; 584 | } 585 | break; 586 | } 587 | } 588 | return { isChanged, changedTask }; 589 | }; 590 | -------------------------------------------------------------------------------- /src/helpers/date-helper.ts: -------------------------------------------------------------------------------- 1 | import { Task, ViewMode } from "../types/public-types"; 2 | import DateTimeFormatOptions = Intl.DateTimeFormatOptions; 3 | import DateTimeFormat = Intl.DateTimeFormat; 4 | 5 | type DateHelperScales = 6 | | "year" 7 | | "month" 8 | | "day" 9 | | "hour" 10 | | "minute" 11 | | "second" 12 | | "millisecond"; 13 | 14 | const intlDTCache = {}; 15 | export const getCachedDateTimeFormat = ( 16 | locString: string | string[], 17 | opts: DateTimeFormatOptions = {} 18 | ): DateTimeFormat => { 19 | const key = JSON.stringify([locString, opts]); 20 | let dtf = intlDTCache[key]; 21 | if (!dtf) { 22 | dtf = new Intl.DateTimeFormat(locString, opts); 23 | intlDTCache[key] = dtf; 24 | } 25 | return dtf; 26 | }; 27 | 28 | export const addToDate = ( 29 | date: Date, 30 | quantity: number, 31 | scale: DateHelperScales 32 | ) => { 33 | const newDate = new Date( 34 | date.getFullYear() + (scale === "year" ? quantity : 0), 35 | date.getMonth() + (scale === "month" ? quantity : 0), 36 | date.getDate() + (scale === "day" ? quantity : 0), 37 | date.getHours() + (scale === "hour" ? quantity : 0), 38 | date.getMinutes() + (scale === "minute" ? quantity : 0), 39 | date.getSeconds() + (scale === "second" ? quantity : 0), 40 | date.getMilliseconds() + (scale === "millisecond" ? quantity : 0) 41 | ); 42 | return newDate; 43 | }; 44 | 45 | export const startOfDate = (date: Date, scale: DateHelperScales) => { 46 | const scores = [ 47 | "millisecond", 48 | "second", 49 | "minute", 50 | "hour", 51 | "day", 52 | "month", 53 | "year", 54 | ]; 55 | 56 | const shouldReset = (_scale: DateHelperScales) => { 57 | const maxScore = scores.indexOf(scale); 58 | return scores.indexOf(_scale) <= maxScore; 59 | }; 60 | const newDate = new Date( 61 | date.getFullYear(), 62 | shouldReset("year") ? 0 : date.getMonth(), 63 | shouldReset("month") ? 1 : date.getDate(), 64 | shouldReset("day") ? 0 : date.getHours(), 65 | shouldReset("hour") ? 0 : date.getMinutes(), 66 | shouldReset("minute") ? 0 : date.getSeconds(), 67 | shouldReset("second") ? 0 : date.getMilliseconds() 68 | ); 69 | return newDate; 70 | }; 71 | 72 | export const ganttDateRange = ( 73 | tasks: Task[], 74 | viewMode: ViewMode, 75 | preStepsCount: number 76 | ) => { 77 | let newStartDate: Date = tasks[0].start; 78 | let newEndDate: Date = tasks[0].start; 79 | for (const task of tasks) { 80 | if (task.start < newStartDate) { 81 | newStartDate = task.start; 82 | } 83 | if (task.end > newEndDate) { 84 | newEndDate = task.end; 85 | } 86 | } 87 | switch (viewMode) { 88 | case ViewMode.Year: 89 | newStartDate = addToDate(newStartDate, -1, "year"); 90 | newStartDate = startOfDate(newStartDate, "year"); 91 | newEndDate = addToDate(newEndDate, 1, "year"); 92 | newEndDate = startOfDate(newEndDate, "year"); 93 | break; 94 | case ViewMode.QuarterYear: 95 | newStartDate = addToDate(newStartDate, -3, "month"); 96 | newStartDate = startOfDate(newStartDate, "month"); 97 | newEndDate = addToDate(newEndDate, 3, "year"); 98 | newEndDate = startOfDate(newEndDate, "year"); 99 | break; 100 | case ViewMode.Month: 101 | newStartDate = addToDate(newStartDate, -1 * preStepsCount, "month"); 102 | newStartDate = startOfDate(newStartDate, "month"); 103 | newEndDate = addToDate(newEndDate, 1, "year"); 104 | newEndDate = startOfDate(newEndDate, "year"); 105 | break; 106 | case ViewMode.Week: 107 | newStartDate = startOfDate(newStartDate, "day"); 108 | newStartDate = addToDate( 109 | getMonday(newStartDate), 110 | -7 * preStepsCount, 111 | "day" 112 | ); 113 | newEndDate = startOfDate(newEndDate, "day"); 114 | newEndDate = addToDate(newEndDate, 1.5, "month"); 115 | break; 116 | case ViewMode.Day: 117 | newStartDate = startOfDate(newStartDate, "day"); 118 | newStartDate = addToDate(newStartDate, -1 * preStepsCount, "day"); 119 | newEndDate = startOfDate(newEndDate, "day"); 120 | newEndDate = addToDate(newEndDate, 19, "day"); 121 | break; 122 | case ViewMode.QuarterDay: 123 | newStartDate = startOfDate(newStartDate, "day"); 124 | newStartDate = addToDate(newStartDate, -1 * preStepsCount, "day"); 125 | newEndDate = startOfDate(newEndDate, "day"); 126 | newEndDate = addToDate(newEndDate, 66, "hour"); // 24(1 day)*3 - 6 127 | break; 128 | case ViewMode.HalfDay: 129 | newStartDate = startOfDate(newStartDate, "day"); 130 | newStartDate = addToDate(newStartDate, -1 * preStepsCount, "day"); 131 | newEndDate = startOfDate(newEndDate, "day"); 132 | newEndDate = addToDate(newEndDate, 108, "hour"); // 24(1 day)*5 - 12 133 | break; 134 | case ViewMode.Hour: 135 | newStartDate = startOfDate(newStartDate, "hour"); 136 | newStartDate = addToDate(newStartDate, -1 * preStepsCount, "hour"); 137 | newEndDate = startOfDate(newEndDate, "day"); 138 | newEndDate = addToDate(newEndDate, 1, "day"); 139 | break; 140 | } 141 | return [newStartDate, newEndDate]; 142 | }; 143 | 144 | export const seedDates = ( 145 | startDate: Date, 146 | endDate: Date, 147 | viewMode: ViewMode 148 | ) => { 149 | let currentDate: Date = new Date(startDate); 150 | const dates: Date[] = [currentDate]; 151 | while (currentDate < endDate) { 152 | switch (viewMode) { 153 | case ViewMode.Year: 154 | currentDate = addToDate(currentDate, 1, "year"); 155 | break; 156 | case ViewMode.QuarterYear: 157 | currentDate = addToDate(currentDate, 3, "month"); 158 | break; 159 | case ViewMode.Month: 160 | currentDate = addToDate(currentDate, 1, "month"); 161 | break; 162 | case ViewMode.Week: 163 | currentDate = addToDate(currentDate, 7, "day"); 164 | break; 165 | case ViewMode.Day: 166 | currentDate = addToDate(currentDate, 1, "day"); 167 | break; 168 | case ViewMode.HalfDay: 169 | currentDate = addToDate(currentDate, 12, "hour"); 170 | break; 171 | case ViewMode.QuarterDay: 172 | currentDate = addToDate(currentDate, 6, "hour"); 173 | break; 174 | case ViewMode.Hour: 175 | currentDate = addToDate(currentDate, 1, "hour"); 176 | break; 177 | } 178 | dates.push(currentDate); 179 | } 180 | return dates; 181 | }; 182 | 183 | export const getLocaleMonth = (date: Date, locale: string) => { 184 | let bottomValue = getCachedDateTimeFormat(locale, { 185 | month: "long", 186 | }).format(date); 187 | bottomValue = bottomValue.replace( 188 | bottomValue[0], 189 | bottomValue[0].toLocaleUpperCase() 190 | ); 191 | return bottomValue; 192 | }; 193 | 194 | export const getLocalDayOfWeek = ( 195 | date: Date, 196 | locale: string, 197 | format?: "long" | "short" | "narrow" | undefined 198 | ) => { 199 | let bottomValue = getCachedDateTimeFormat(locale, { 200 | weekday: format, 201 | }).format(date); 202 | bottomValue = bottomValue.replace( 203 | bottomValue[0], 204 | bottomValue[0].toLocaleUpperCase() 205 | ); 206 | return bottomValue; 207 | }; 208 | 209 | /** 210 | * Returns monday of current week 211 | * @param date date for modify 212 | */ 213 | const getMonday = (date: Date) => { 214 | const day = date.getDay(); 215 | const diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday 216 | return new Date(date.setDate(diff)); 217 | }; 218 | 219 | export const getWeekNumberISO8601 = (date: Date) => { 220 | const tmpDate = new Date(date.valueOf()); 221 | const dayNumber = (tmpDate.getDay() + 6) % 7; 222 | tmpDate.setDate(tmpDate.getDate() - dayNumber + 3); 223 | const firstThursday = tmpDate.valueOf(); 224 | tmpDate.setMonth(0, 1); 225 | if (tmpDate.getDay() !== 4) { 226 | tmpDate.setMonth(0, 1 + ((4 - tmpDate.getDay() + 7) % 7)); 227 | } 228 | const weekNumber = ( 229 | 1 + Math.ceil((firstThursday - tmpDate.valueOf()) / 604800000) 230 | ).toString(); 231 | 232 | if (weekNumber.length === 1) { 233 | return `0${weekNumber}`; 234 | } else { 235 | return weekNumber; 236 | } 237 | }; 238 | 239 | export const getDaysInMonth = (month: number, year: number) => { 240 | return new Date(year, month + 1, 0).getDate(); 241 | }; 242 | -------------------------------------------------------------------------------- /src/helpers/other-helper.ts: -------------------------------------------------------------------------------- 1 | import { BarTask } from "../types/bar-task"; 2 | import { Task } from "../types/public-types"; 3 | 4 | export function isKeyboardEvent( 5 | event: React.MouseEvent | React.KeyboardEvent | React.FocusEvent 6 | ): event is React.KeyboardEvent { 7 | return (event as React.KeyboardEvent).key !== undefined; 8 | } 9 | 10 | export function isMouseEvent( 11 | event: React.MouseEvent | React.KeyboardEvent | React.FocusEvent 12 | ): event is React.MouseEvent { 13 | return (event as React.MouseEvent).clientX !== undefined; 14 | } 15 | 16 | export function isBarTask(task: Task | BarTask): task is BarTask { 17 | return (task as BarTask).x1 !== undefined; 18 | } 19 | 20 | export function removeHiddenTasks(tasks: Task[]) { 21 | const groupedTasks = tasks.filter( 22 | t => t.hideChildren && t.type === "project" 23 | ); 24 | if (groupedTasks.length > 0) { 25 | for (let i = 0; groupedTasks.length > i; i++) { 26 | const groupedTask = groupedTasks[i]; 27 | const children = getChildren(tasks, groupedTask); 28 | tasks = tasks.filter(t => children.indexOf(t) === -1); 29 | } 30 | } 31 | return tasks; 32 | } 33 | 34 | function getChildren(taskList: Task[], task: Task) { 35 | let tasks: Task[] = []; 36 | if (task.type !== "project") { 37 | tasks = taskList.filter( 38 | t => t.dependencies && t.dependencies.indexOf(task.id) !== -1 39 | ); 40 | } else { 41 | tasks = taskList.filter(t => t.project && t.project === task.id); 42 | } 43 | var taskChildren: Task[] = []; 44 | tasks.forEach(t => { 45 | taskChildren.push(...getChildren(taskList, t)); 46 | }) 47 | tasks = tasks.concat(tasks, taskChildren); 48 | return tasks; 49 | } 50 | 51 | export const sortTasks = (taskA: Task, taskB: Task) => { 52 | const orderA = taskA.displayOrder || Number.MAX_VALUE; 53 | const orderB = taskB.displayOrder || Number.MAX_VALUE; 54 | if (orderA > orderB) { 55 | return 1; 56 | } else if (orderA < orderB) { 57 | return -1; 58 | } else { 59 | return 0; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { Gantt } from "./components/gantt/gantt"; 2 | export { ViewMode } from "./types/public-types"; 3 | export type { 4 | GanttProps, 5 | Task, 6 | StylingOption, 7 | DisplayOption, 8 | EventOption, 9 | } from "./types/public-types"; 10 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/test/date-helper.test.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | seedDates, 3 | addToDate, 4 | getWeekNumberISO8601, 5 | } from "../helpers/date-helper"; 6 | import { ViewMode } from "../types/public-types"; 7 | 8 | describe("seed date", () => { 9 | test("daily", () => { 10 | expect( 11 | seedDates(new Date(2020, 5, 28), new Date(2020, 6, 2), ViewMode.Day) 12 | ).toEqual([ 13 | new Date(2020, 5, 28), 14 | new Date(2020, 5, 29), 15 | new Date(2020, 5, 30), 16 | new Date(2020, 6, 1), 17 | new Date(2020, 6, 2), 18 | ]); 19 | }); 20 | 21 | test("weekly", () => { 22 | expect( 23 | seedDates(new Date(2020, 5, 28), new Date(2020, 6, 19), ViewMode.Week) 24 | ).toEqual([ 25 | new Date(2020, 5, 28), 26 | new Date(2020, 6, 5), 27 | new Date(2020, 6, 12), 28 | new Date(2020, 6, 19), 29 | ]); 30 | }); 31 | 32 | test("monthly", () => { 33 | expect( 34 | seedDates(new Date(2020, 5, 28), new Date(2020, 6, 19), ViewMode.Month) 35 | ).toEqual([new Date(2020, 5, 28), new Date(2020, 6, 28)]); 36 | }); 37 | 38 | test("quarterly", () => { 39 | expect( 40 | seedDates( 41 | new Date(2020, 5, 28), 42 | new Date(2020, 5, 29), 43 | ViewMode.QuarterDay 44 | ) 45 | ).toEqual([ 46 | new Date(2020, 5, 28, 0, 0), 47 | new Date(2020, 5, 28, 6, 0), 48 | new Date(2020, 5, 28, 12, 0), 49 | new Date(2020, 5, 28, 18, 0), 50 | new Date(2020, 5, 29, 0, 0), 51 | ]); 52 | }); 53 | }); 54 | 55 | describe("add to date", () => { 56 | test("add month", () => { 57 | expect(addToDate(new Date(2020, 0, 1), 40, "month")).toEqual( 58 | new Date(2023, 4, 1) 59 | ); 60 | }); 61 | 62 | test("add day", () => { 63 | expect(addToDate(new Date(2020, 0, 1), 40, "day")).toEqual( 64 | new Date(2020, 1, 10) 65 | ); 66 | }); 67 | }); 68 | 69 | test("get week number", () => { 70 | expect(getWeekNumberISO8601(new Date(2019, 11, 31))).toEqual("01"); 71 | expect(getWeekNumberISO8601(new Date(2021, 0, 1))).toEqual("53"); 72 | expect(getWeekNumberISO8601(new Date(2020, 6, 20))).toEqual("30"); 73 | }); 74 | -------------------------------------------------------------------------------- /src/test/gant.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { Gantt } from "../index"; 4 | 5 | describe("gantt", () => { 6 | it("renders without crashing", () => { 7 | const div = document.createElement("div"); 8 | const root = createRoot(div); 9 | root.render( 10 | 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/types/bar-task.ts: -------------------------------------------------------------------------------- 1 | import { Task, TaskType } from "./public-types"; 2 | 3 | export interface BarTask extends Task { 4 | index: number; 5 | typeInternal: TaskTypeInternal; 6 | x1: number; 7 | x2: number; 8 | y: number; 9 | height: number; 10 | progressX: number; 11 | progressWidth: number; 12 | barCornerRadius: number; 13 | handleWidth: number; 14 | barChildren: BarTask[]; 15 | styles: { 16 | backgroundColor: string; 17 | backgroundSelectedColor: string; 18 | progressColor: string; 19 | progressSelectedColor: string; 20 | }; 21 | } 22 | 23 | export type TaskTypeInternal = TaskType | "smalltask"; 24 | -------------------------------------------------------------------------------- /src/types/date-setup.ts: -------------------------------------------------------------------------------- 1 | import { ViewMode } from "./public-types"; 2 | 3 | export interface DateSetup { 4 | dates: Date[]; 5 | viewMode: ViewMode; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/gantt-task-actions.ts: -------------------------------------------------------------------------------- 1 | import { BarTask } from "./bar-task"; 2 | 3 | export type BarMoveAction = "progress" | "end" | "start" | "move"; 4 | export type GanttContentMoveAction = 5 | | "mouseenter" 6 | | "mouseleave" 7 | | "delete" 8 | | "dblclick" 9 | | "click" 10 | | "select" 11 | | "" 12 | | BarMoveAction; 13 | 14 | export type GanttEvent = { 15 | changedTask?: BarTask; 16 | originalSelectedTask?: BarTask; 17 | action: GanttContentMoveAction; 18 | }; 19 | -------------------------------------------------------------------------------- /src/types/public-types.ts: -------------------------------------------------------------------------------- 1 | export enum ViewMode { 2 | Hour = "Hour", 3 | QuarterDay = "Quarter Day", 4 | HalfDay = "Half Day", 5 | Day = "Day", 6 | /** ISO-8601 week */ 7 | Week = "Week", 8 | Month = "Month", 9 | QuarterYear = "QuarterYear", 10 | Year = "Year", 11 | } 12 | export type TaskType = "task" | "milestone" | "project"; 13 | export interface Task { 14 | id: string; 15 | type: TaskType; 16 | name: string; 17 | start: Date; 18 | end: Date; 19 | /** 20 | * From 0 to 100 21 | */ 22 | progress: number; 23 | styles?: { 24 | backgroundColor?: string; 25 | backgroundSelectedColor?: string; 26 | progressColor?: string; 27 | progressSelectedColor?: string; 28 | }; 29 | isDisabled?: boolean; 30 | project?: string; 31 | dependencies?: string[]; 32 | hideChildren?: boolean; 33 | displayOrder?: number; 34 | } 35 | 36 | export interface EventOption { 37 | /** 38 | * Time step value for date changes. 39 | */ 40 | timeStep?: number; 41 | /** 42 | * Invokes on bar select on unselect. 43 | */ 44 | onSelect?: (task: Task, isSelected: boolean) => void; 45 | /** 46 | * Invokes on bar double click. 47 | */ 48 | onDoubleClick?: (task: Task) => void; 49 | /** 50 | * Invokes on bar click. 51 | */ 52 | onClick?: (task: Task) => void; 53 | /** 54 | * Invokes on end and start time change. Chart undoes operation if method return false or error. 55 | */ 56 | onDateChange?: ( 57 | task: Task, 58 | children: Task[] 59 | ) => void | boolean | Promise | Promise; 60 | /** 61 | * Invokes on progress change. Chart undoes operation if method return false or error. 62 | */ 63 | onProgressChange?: ( 64 | task: Task, 65 | children: Task[] 66 | ) => void | boolean | Promise | Promise; 67 | /** 68 | * Invokes on delete selected task. Chart undoes operation if method return false or error. 69 | */ 70 | onDelete?: (task: Task) => void | boolean | Promise | Promise; 71 | /** 72 | * Invokes on expander on task list 73 | */ 74 | onExpanderClick?: (task: Task) => void; 75 | } 76 | 77 | export interface DisplayOption { 78 | viewMode?: ViewMode; 79 | viewDate?: Date; 80 | preStepsCount?: number; 81 | /** 82 | * Specifies the month name language. Able formats: ISO 639-2, Java Locale 83 | */ 84 | locale?: string; 85 | rtl?: boolean; 86 | } 87 | 88 | export interface StylingOption { 89 | headerHeight?: number; 90 | columnWidth?: number; 91 | listCellWidth?: string; 92 | rowHeight?: number; 93 | ganttHeight?: number; 94 | barCornerRadius?: number; 95 | handleWidth?: number; 96 | fontFamily?: string; 97 | fontSize?: string; 98 | /** 99 | * How many of row width can be taken by task. 100 | * From 0 to 100 101 | */ 102 | barFill?: number; 103 | barProgressColor?: string; 104 | barProgressSelectedColor?: string; 105 | barBackgroundColor?: string; 106 | barBackgroundSelectedColor?: string; 107 | projectProgressColor?: string; 108 | projectProgressSelectedColor?: string; 109 | projectBackgroundColor?: string; 110 | projectBackgroundSelectedColor?: string; 111 | milestoneBackgroundColor?: string; 112 | milestoneBackgroundSelectedColor?: string; 113 | arrowColor?: string; 114 | arrowIndent?: number; 115 | todayColor?: string; 116 | TooltipContent?: React.FC<{ 117 | task: Task; 118 | fontSize: string; 119 | fontFamily: string; 120 | }>; 121 | TaskListHeader?: React.FC<{ 122 | headerHeight: number; 123 | rowWidth: string; 124 | fontFamily: string; 125 | fontSize: string; 126 | }>; 127 | TaskListTable?: React.FC<{ 128 | rowHeight: number; 129 | rowWidth: string; 130 | fontFamily: string; 131 | fontSize: string; 132 | locale: string; 133 | tasks: Task[]; 134 | selectedTaskId: string; 135 | /** 136 | * Sets selected task by id 137 | */ 138 | setSelectedTask: (taskId: string) => void; 139 | onExpanderClick: (task: Task) => void; 140 | }>; 141 | } 142 | 143 | export interface GanttProps extends EventOption, DisplayOption, StylingOption { 144 | tasks: Task[]; 145 | } 146 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default CSS definition for typescript, 3 | * will be overridden with file-specific definitions by rollup 4 | */ 5 | declare module "*.css" { 6 | const content: { [className: string]: string }; 7 | export default content; 8 | } 9 | 10 | interface SvgrComponent 11 | extends React.StatelessComponent> {} 12 | 13 | declare module "*.svg" { 14 | const svgUrl: string; 15 | const svgComponent: SvgrComponent; 16 | export default svgUrl; 17 | export { svgComponent as ReactComponent }; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "allowSyntheticDefaultImports": true, 22 | "target": "es5", 23 | "allowJs": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "resolveJsonModule": true, 28 | "isolatedModules": true, 29 | "noEmit": true 30 | }, 31 | "include": [ 32 | "src" 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | "dist", 37 | "example" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | --------------------------------------------------------------------------------