Skip to content

Instantly share code, notes, and snippets.

@redanium
Created February 25, 2025 21:57
Show Gist options
  • Save redanium/9bd34740f5141738ed09e342f9174cf0 to your computer and use it in GitHub Desktop.
Save redanium/9bd34740f5141738ed09e342f9174cf0 to your computer and use it in GitHub Desktop.

├── .github ├── gantt-logo.jpg ├── hero-image.png └── workflows │ └── publish.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── builder ├── demo.css └── demo.js ├── eslint.config.mjs ├── index.html ├── license.txt ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── src ├── arrow.js ├── bar.js ├── date_utils.js ├── defaults.js ├── index.js ├── popup.js ├── styles │ ├── dark.css │ ├── gantt.css │ └── light.css └── svg_utils.js ├── tests └── date_utils.test.js └── vite.config.js

/.github/gantt-logo.jpg:

https://raw.githubusercontent.com/frappe/gantt/108eeb5898043eb870a47617a878bb253237c324/.github/gantt-logo.jpg


/.github/hero-image.png:

https://raw.githubusercontent.com/frappe/gantt/108eeb5898043eb870a47617a878bb253237c324/.github/hero-image.png


/.github/workflows/publish.yml:

1 | name: Publish on NPM 2 | on: 3 | push: 4 | branches: [release] 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: pnpm/action-setup@v4 12 | with: 13 | version: 9 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '18' 17 | cache: 'pnpm' 18 | - run: pnpm install 19 | - run: pnpm prettier-check 20 | - run: pnpm build 21 | - uses: JS-DevTools/npm-publish@v1 22 | with: 23 | token: ${{ secrets.NPM_TOKEN }} 24 |


/.gitignore:

1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | .pid 8 | .seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | dist/ 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | .yarn 30 | 31 | .DS_Store 32 | 33 | gh-pages 34 | feedback.md 35 |


/.prettierignore:

1 | dist


/.prettierrc.json:

1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true 4 | } 5 |


/README.md:

1 |

2 | 3 |

Frappe Gantt

4 | 5 | A modern, configurable, Gantt library for the web. 6 |
7 | 8 |

Hero Image

9 | 10 | ## Frappe Gantt 11 | Gantt charts are bar charts that visually illustrate a project's tasks, schedule, and dependencies. With Frappe Gantt, you can build beautiful, customizable, Gantt charts with ease. 12 | 13 | You can use it anywhere from hobby projects to tracking the goals of your team at the worksplace. 14 | 15 | ERPNext uses Frappe Gantt. 16 | 17 | 18 | ### Motivation 19 | We needed a Gantt View for ERPNext. Surprisingly, we couldn't find a visually appealing Gantt library that was open source - so we decided to build it. Initially, the design was heavily inspired by Google Gantt and DHTMLX. 20 | 21 | 22 | ### Key Features 23 | - Customizable Views: customize the timeline based on various time periods - day, hour, or year, you have it. You can also create your own views. 24 | - Ignore Periods: exclude weekends and other holidays from your tasks' progress calculation. 25 | - Configure Anything: spacing, edit access, labels, you can control it all. Change both the style and functionality to meet your needs. 26 | - Multi-lingual Support: suitable for companies with an international base. 27 | 28 | ## Usage 29 | 30 | Install with: 31 | bash 32 | npm install frappe-gantt 33 | 34 | 35 | Include it in your HTML: 36 | 37 | html 38 | <script src="frappe-gantt.umd.js"></script> 39 | <link rel="stylesheet" href="frappe-gantt.css"> 40 | 41 | 42 | Or from the CDN: 43 | html 44 | <script src="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.umd.js"></script> 45 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.css"> 46 | 47 | 48 | Start using Gantt: 49 | js 50 | let tasks = [ 51 | { 52 | id: '1', 53 | name: 'Redesign website', 54 | start: '2016-12-28', 55 | end: '2016-12-31', 56 | progress: 20 57 | }, 58 | ... 59 | ] 60 | let gantt = new Gantt("#gantt", tasks); 61 | 62 | 63 | ### Configuration 64 | Frappe Gantt offers a wide range of options to customize your chart. 65 | 66 | 67 | | Option | Description | Possible Values | Default | 68 | |---------------------------|---------------------------------------------------------------------------------|----------------------------------------------------|------------------------------------| 69 | | arrow_curve | Curve radius of arrows connecting dependencies. | Any positive integer. | 5 | 70 | | auto_move_label | Move task labels when user scrolls horizontally. | true, false | false | 71 | | bar_corner_radius | Radius of the task bar corners (in pixels). | Any positive integer. | 3 | 72 | | bar_height | Height of task bars (in pixels). | Any positive integer. | 30 | 73 | | container_height | Height of the container. | auto - dynamic container height to fit all tasks - or any positive integer (for pixels). | auto | 74 | | column_width | Width of each column in the timeline. | Any positive integer. | 45 | 75 | | date_format | Format for displaying dates. | Any valid JS date format string. | YYYY-MM-DD | 76 | | upper_header_height | Height of the upper header in the timeline (in pixels). | Any positive integer. | 45 | 77 | | lower_header_height | Height of the lower header in the timeline (in pixels). | Any positive integer. | 30 | 78 | | snap_at | Snap tasks at particular intervel while resizing or dragging. | Any interval (see below) | 1d | 79 | | infinite_padding | Whether to extend timeline infinitely when user scrolls. | true, false | true | 80 | | holidays | Highlighted holidays on the timeline. | Object mapping CSS colors to holiday types. Types can either be a) 'weekend', or b) array of strings or date objects or objects in the format {date: ..., label: ...} | { 'var(--g-weekend-highlight-color)': 'weekend' } | 81 | | ignore | Ignored areas in the rendering | weekend or Array of strings or date objects (weekend can be present to the array also). | [] | 82 | | language | Language for localization. | ISO 639-1 codes like en, fr, es. | en | 83 | | lines | Determines which grid lines to display. | none for no lines, vertical for only vertical lines, horizontal for only horizontal lines, both for complete grid. | both | 84 | | move_dependencies | Whether moving a task automatically moves its dependencies. | true, false | true | 85 | | padding | Padding around task bars (in pixels). | Any positive integer. | 18 | 86 | | popup_on | Event to trigger the popup display. | click or hover | click | 87 | | readonly_progress | Disables editing task progress. | true, false | false | 88 | | readonly_dates | Disables editing task dates. | true, false | false | 89 | | readonly | Disables all editing features. | true, false | false | 90 | | scroll_to | Determines the starting point when chart is rendered. | today, start, end, or a date string. | today | 91 | | show_expected_progress | Shows expected progress for tasks. | true, false | false | 92 | | today_button | Adds a button to navigate to today’s date. | true, false | true | 93 | | view_mode | The initial view mode of the Gantt chart. | Day, Week, Month, Year. | Day | 94 | | view_mode_select | Allows selecting the view mode from a dropdown. | true, false | false | 95 | 96 | Apart from these ones, two options - popup and view_modes (plural, not singular) - are available. They have "sub"-APIs, and thus are listed separately. 97 | 98 | #### View Mode Configuration 99 | The view_modes option determines all the available view modes for the chart. It should be an array of objects. 100 | 101 | Each object can have the following properties: 102 | - name (string) - the name of view mode. 103 | - padding (interval) - the time above. 104 | - step - the interval of each column 105 | - lower_text (date format string or function) - the format for text in lower header. Blank string for none. The function takes in currentDate, previousDate, and lang, and should return a string. 106 | - upper_text (date format string or function) - the format for text in upper header. Blank string for none. The function takes in currentDate, previousDate, and lang, and should return a string. 107 | - upper_text_frequency (number) - how often the upper text has a value. Utilized in internal calculation to improve performance. 108 | - thick_line (function) - takes in currentDate, returns Boolean determining whether the line for that date should be thicker than the others. 109 | 110 | Three other options allow you to override general configuration for this view mode alone: 111 | - date_format 112 | - column_width 113 | - snap_at 114 | For details, see the above table. 115 | 116 | #### Popup Configuration 117 | popup is a function. If it returns 118 | - false, there will be no popup. 119 | - undefined, the popup will be rendered based on manipulation within the function 120 | - a HTML string, the popup will be that string. 121 | 122 | The function receives one object as an argument, containing: 123 | - task - the task as an object 124 | - chart - the entire Gantt chart 125 | - get_title, get_subtitle, get_details (functions) - get the relevant section as a HTML node. 126 | - set_title, set_subtitle, set_details (functions) - take in the HTML of the relevant section 127 | - add_action (function) - accepts two parameters, html and func - respectively determining the HTML of the action and the callback when the action is pressed. 128 | 129 | ### API 130 | Frappe Gantt exposes a few helpful methods for you to interact with the chart: 131 | 132 | | Name | Description | Parameters | 133 | |---------------------------|---------------------------------------------------------------------------------|------------------------------------------| 134 | | .update_options | Re-renders the chart after updating specific options. | new_options - object containing new options. | 135 | | .change_view_mode | Updates the view mode. | view_mode - Name of view mode or view mode object (see above) and maintain_pos - whether to go back to current scroll position after rerendering, defaults to false. | 136 | | .scroll_current | Scrolls to the current date | No parameters. | 137 | | .update_task | Re-renders a specific task bar alone | task_id - id of task and new_details - object containing the task properties to be updated. | 138 | 139 | ## Development Setup 140 | If you want to contribute enhancements or fixes: 141 | 142 | 1. Clone this repo. 143 | 2. cd into project directory. 144 | 3. Run pnpm i to install dependencies. 145 | 4. pnpm run build to build files - or pnpm run build-dev to build and watch for changes. 146 | 5. Open index.html in your browser. 147 | 6. Make your code changes and test them. 148 | 149 |
150 |
151 | 159 |


/builder/demo.css:

1 | .switch { 2 | position: relative; 3 | display: inline-block; 4 | width: 50px; 5 | height: 20px; 6 | float: right; 7 | } 8 | 9 | .switch input { 10 | opacity: 0; 11 | width: 0; 12 | height: 0; 13 | } 14 | 15 | .slider { 16 | position: absolute; 17 | cursor: pointer; 18 | top: 0; 19 | left: 0; 20 | right: 0; 21 | bottom: 0; 22 | background-color: #ddd; 23 | -webkit-transition: 0.2s; 24 | transition: 0.2s; 25 | border: 1px solid #37352f; 26 | scale: 0.75; 27 | } 28 | 29 | .slider:before { 30 | position: absolute; 31 | content: ''; 32 | height: 12px; 33 | width: 12px; 34 | left: 4px; 35 | bottom: 3px; 36 | background-color: white; 37 | -webkit-transition: 0.2s; 38 | transition: 0.2s; 39 | } 40 | 41 | input:checked + .slider { 42 | background-color: #7c7c7c; 43 | border-color: #7c7c7c; 44 | } 45 | 46 | input:focus + .slider { 47 | box-shadow: none; 48 | } 49 | 50 | input:checked + .slider:before { 51 | -webkit-transform: translateX(28px); 52 | -ms-transform: translateX(28px); 53 | transform: translateX(28px); 54 | } 55 | 56 | .slider.round { 57 | border-radius: 25px; 58 | } 59 | 60 | .slider.round:before { 61 | border-radius: 50%; 62 | } 63 | 64 | .viewmode-select { 65 | font-size: 100%; 66 | } 67 | 68 | .selected { 69 | border: 1.5px solid black !important; 70 | } 71 | 72 | .button { 73 | background: white; 74 | border: 1px dotted black; 75 | border-radius: 3px; 76 | } 77 | 78 | .button:hover { 79 | background: #f4f5f6; 80 | border: 1px dotted black; 81 | } 82 | 83 | .button div { 84 | color: black; 85 | } 86 | 87 | .input-switch { 88 | align-items: center; 89 | width: 45%; 90 | display: flex; 91 | justify-content: space-between; 92 | } 93 | 94 | .input-switch label { 95 | padding-right: 30px; 96 | font-size: 14px; 97 | } 98 | 99 | .code { 100 | display: block; 101 | background: 0; 102 | white-space: pre; 103 | overflow-x: scroll; 104 | max-width: 100%; 105 | min-width: 100px; 106 | padding: 0; 107 | font-family: monospace; 108 | padding-top: 0.8571429em; 109 | padding-right: 1.1428571em; 110 | padding-bottom: 0.8571429em; 111 | padding-left: 1.1428571em; 112 | background: #1f2937; 113 | color: #e5e7eb; 114 | border-radius: 3px; 115 | } 116 |


/builder/demo.js:

1 | const tasks = [ 2 | { 3 | start: daysSince(-7), 4 | end: daysSince(-5), 5 | name: 'Initial brainstorming', 6 | id: 'Task 0', 7 | progress: random(), 8 | }, 9 | { 10 | start: daysSince(-3), 11 | end: daysSince(1), 12 | name: 'Develop wireframe', 13 | id: 'Task 1', 14 | progress: random(), 15 | dependencies: 'Task 0', 16 | }, 17 | { 18 | start: daysSince(-1), 19 | duration: '4d', 20 | name: 'Client meeting', 21 | id: 'Task 2', 22 | progress: random(), 23 | important: true, 24 | }, 25 | { 26 | start: daysSince(1), 27 | duration: '7d', 28 | name: 'Create prototype', 29 | id: 'Task 3', 30 | dependencies: 'Task 2', 31 | progress: random(), 32 | }, 33 | { 34 | start: daysSince(3), 35 | duration: '5d', 36 | name: 'Test design with users', 37 | dependencies: 'Task 2', 38 | id: 'Task 4', 39 | progress: random(), 40 | important: true, 41 | }, 42 | { 43 | start: daysSince(5), 44 | end: daysSince(10), 45 | name: 'Write technical documentation', 46 | id: 'Task 5', 47 | progress: random(), 48 | }, 49 | { 50 | start: daysSince(8), 51 | duration: '3d', 52 | name: 'Prepare demo', 53 | id: 'Task 6', 54 | dependencies: 'Task 5', 55 | progress: random(), 56 | }, 57 | { 58 | start: daysSince(10), 59 | end: daysSince(12), 60 | name: 'Final client review', 61 | id: 'Task 7', 62 | progress: 0, 63 | important: true, 64 | }, 65 | { 66 | start: daysSince(14), 67 | duration: '6d', 68 | name: 'Implement feedback', 69 | id: 'Task 8', 70 | progress: 0, 71 | }, 72 | ]; 73 | 74 | const tasksSmall = [ 75 | { 76 | start: daysSince(-2), 77 | end: daysSince(2), 78 | name: 'Redesign website', 79 | id: 'Task 0', 80 | progress: random(), 81 | }, 82 | { 83 | start: daysSince(3), 84 | duration: '6d', 85 | name: 'Write new content', 86 | id: 'Task 1', 87 | progress: random(), 88 | important: true, 89 | dependencies: 'Task 0', 90 | }, 91 | { 92 | start: daysSince(4), 93 | duration: '2d', 94 | name: 'Apply new styles', 95 | id: 'Task 2', 96 | progress: random(), 97 | }, 98 | { 99 | start: daysSince(-4), 100 | end: daysSince(0), 101 | name: 'Review', 102 | id: 'Task 3', 103 | progress: random(), 104 | }, 105 | ]; 106 | 107 | const tasksBlank = [ 108 | { 109 | start: daysSince(1), 110 | duration: '3d', 111 | name: 'Marketing Strategy Review', 112 | id: 'Task 1', 113 | important: true, 114 | }, 115 | { 116 | start: daysSince(-2), 117 | end: daysSince(12), 118 | name: 'Mentor Sooriya', 119 | id: 'Task 0', 120 | }, 121 | { 122 | start: daysSince(4), 123 | end: daysSince(5), 124 | name: 'Investors Meetup', 125 | id: 'Task 3', 126 | }, 127 | ]; 128 | 129 | const HOLIDAYS = [ 130 | { name: 'New Years Day', date: '2025-01-01' }, 131 | { name: 'Republic Day', date: '2025-01-26' }, 132 | { name: 'Maha Shivratri', date: '2025-02-23' }, 133 | { name: 'Holi', date: '2025-03-11' }, 134 | { name: 'Mahavir Jayanthi', date: '2025-04-07' }, 135 | { name: 'Good Friday', date: '2025-04-10' }, 136 | { name: 'May Day', date: '2025-05-01' }, 137 | { name: 'Buddha Purnima', date: '2025-05-08' }, 138 | { name: 'Krishna Janmastami', date: '2025-08-14' }, 139 | { name: 'Independence Day', date: '2025-08-15' }, 140 | { name: 'Ganesh Chaturthi', date: '2025-08-23' }, 141 | { name: 'Id-Ul-Fitr', date: '2025-09-21' }, 142 | { name: 'Vijaya Dashami', date: '2025-09-28' }, 143 | { name: 'Mahatma Gandhi Jayanti', date: '2025-10-02' }, 144 | { name: 'Diwali', date: '2025-10-17' }, 145 | { name: 'Guru Nanak Jayanthi', date: '2025-11-02' }, 146 | { name: 'Christmas', date: '2025-12-25' }, 147 | ]; 148 | 149 | new Gantt('#central-demo', tasks, { 150 | scroll_to: daysSince(-7), 151 | infinite_padding: false, 152 | }); 153 | 154 | const sideheader = new Gantt('#sideheader', tasksSmall, { 155 | scroll_to: daysSince(-20), 156 | view_mode_select: true, 157 | infinite_padding: false, 158 | }); 159 | 160 | const popup = new Gantt('#popup', tasksBlank, { 161 | scroll_to: daysSince(-7), 162 | infinite_padding: false, 163 | container_height: 350, 164 | popup: (ctx) => { 165 | ctx.set_title(ctx.task.name); 166 | let title = ctx.get_title(); 167 | title.style.border = '0.5px solid black'; 168 | title.style.borderRadius = '1.5px'; 169 | title.style.padding = '3px 5px '; 170 | title.style.backgroundColor = 'black'; 171 | title.style.opacity = '0.85'; 172 | title.style.color = 'white'; 173 | title.style.width = 'fit-content'; 174 | title.onclick = () => { 175 | let ans = prompt('New Title: '); 176 | if (ans) ctx.set_title(ans); 177 | }; 178 | if (ctx.task.description) ctx.set_subtitle(ctx.task.description); 179 | else ctx.set_subtitle(''); 180 | 181 | ctx.set_details( 182 | <em>Duration</em>: ${ctx.task.actual_duration} days<br/><em>Dates</em>: ${ctx.task._start.toLocaleDateString('en-US')} - ${ctx.task._end.toLocaleDateString('en-US')}, 183 | ); 184 | let details = ctx.get_details(); 185 | details.style.lineHeight = '1.75'; 186 | details.style.margin = '10px 4px'; 187 | if (!ctx.chart.options.readonly) { 188 | if (!ctx.chart.options.readonly_progress) { 189 | ctx.add_action('Set Color', (task, chart) => { 190 | const bar = chart.bars.find( 191 | ({ task: t }) => t.id === task.id, 192 | ).$bar; 193 | bar.style.fill = hsla(${~~(360 * Math.random())}, 70%, 72%, 0.8); 194 | }); 195 | } 196 | } 197 | }, 198 | }); 199 | 200 | const holidays = new Gantt('#holidays', tasks, { 201 | holidays: { 202 | 'var(--g-weekend-highlight-color)': [], 203 | '#fffddb': HOLIDAYS, 204 | }, 205 | ignore: ['weekend'], 206 | infinite_padding: false, 207 | container_height: 350, 208 | scroll_to: daysSince(-7), 209 | }); 210 | 211 | SWITCHES = { 212 | 'sideheader-form': { 213 | 'toggle-today': 'Scroll to today: ', 214 | 'toggle-view-mode': 'Change view mode: ', 215 | }, 216 | 'holiday-subform': { 217 | 'toggle-weekends': ['Mark weekends: ', false], 218 | 'ignore-weekends': 'Exclude weekends: ', 219 | }, 220 | }; 221 | 222 | for (let form of ['sideheader-form', 'holiday-form']) { 223 | let formNode = document.getElementById(form); 224 | let parent = formNode.parentElement; 225 | parent.appendChild(formNode); 226 | } 227 | 228 | for (let form in SWITCHES) { 229 | for (let id in SWITCHES[form]) { 230 | createSwitch(form, id, SWITCHES[form][id]); 231 | } 232 | } 233 | 234 | const UPDATES = [ 235 | [ 236 | sideheader, 237 | { 238 | 'toggle-today': 'today_button', 239 | 'toggle-view-mode': 'view_mode_select', 240 | }, 241 | ], 242 | [ 243 | holidays, 244 | { 245 | 'toggle-weekends': (val, opts) => ({ 246 | holidays: { 247 | '#fffddb': opts.holidays['#fffddb'], 248 | 'var(--g-weekend-highlight-color)': val ? 'weekend' : [], 249 | }, 250 | ignore: [], 251 | }), 252 | 'declare-holiday': (val, opts) => ({ 253 | holidays: { 254 | '#fffddb': [...HOLIDAYS, { date: val, name: 'Kay' }], 255 | 'var(--g-weekend-highlight-color)': 256 | opts.holidays['var(--g-weekend-highlight-color)'], 257 | }, 258 | }), 259 | 'ignore-weekends': (val, opts) => ({ 260 | ignore: [ 261 | opts.ignore.filter((k) => k !== 'weekend')[0], 262 | ...(val ? ['weekend'] : []), 263 | ], 264 | holidays: { '#fffddb': opts.holidays['#fffddb'] }, 265 | }), 266 | 'declare-ignore': (val, opts) => ({ 267 | ignore: [ 268 | ...(opts.ignore.includes('weekend') ? ['weekend'] : []), 269 | val, 270 | ], 271 | }), 272 | }, 273 | (id, val) => { 274 | let el = document.getElementById(id); 275 | if (id === 'toggle-weekends' && val) { 276 | document.getElementById('ignore-weekends').checked = false; 277 | } 278 | if (id === 'ignore-weekends' && val) { 279 | document.getElementById('toggle-weekends').checked = false; 280 | } 281 | }, 282 | ], 283 | ]; 284 | 285 | for (let [chart, details, after] of UPDATES) { 286 | for (let id in details) { 287 | let el = document.getElementById(id); 288 | 289 | el.onchange = (e) => { 290 | let label = details[id]; 291 | let val; 292 | if (e.currentTarget.type === 'checkbox') { 293 | if (typeof label === 'string') { 294 | let opposite = label.slice(0, 5) === 'opp__'; 295 | if (opposite) label = label.slice(5); 296 | val = opposite 297 | ? !e.currentTarget.checked 298 | : e.currentTarget.checked; 299 | } else if (typeof label === 'object') { 300 | val = label[e.currentTarget.checked ? 1 : 2]; 301 | label = label[0]; 302 | } else { 303 | val = 304 | e.currentTarget.type === 'checkbox' 305 | ? e.currentTarget.checked 306 | : e.currentTarget.value; 307 | } 308 | } else { 309 | val = 310 | e.currentTarget.type === 'date' 311 | ? e.currentTarget.value 312 | : +e.currentTarget.value; 313 | } 314 | 315 | if (typeof label === 'function') { 316 | console.log('ha', label(val, chart.options)); 317 | chart.update_options(label(val, chart.options)); 318 | } else { 319 | chart.update_options({ 320 | [label]: val, 321 | }); 322 | } 323 | after && after(id, val, chart); 324 | }; 325 | } 326 | } 327 |


/eslint.config.mjs:

1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | import js from "@eslint/js"; 4 | import { FlatCompat } from "@eslint/eslintrc"; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | recommendedConfig: js.configs.recommended, 11 | allConfig: js.configs.all 12 | }); 13 | 14 | export default [...compat.extends("plugin:prettier/recommended"), { 15 | languageOptions: { 16 | ecmaVersion: 6, 17 | sourceType: "module", 18 | }, 19 | }];


/index.html:

1 | <!doctype html> 2 | 3 | 4 | 5 | <title>Simple Gantt</title> 6 | 7 | <link 8 | href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" 9 | rel="stylesheet" 10 | integrity_no="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" 11 | crossorigin="anonymous" 12 | /> 13 | <style> 14 | .container { 15 | width: 90%; 16 | margin: 0 auto; 17 | } 18 | 19 | .chart { 20 | border: 1px dotted black; 21 | border-radius: 4px; 22 | height: fit-content; 23 | } 24 | 25 | .chart.active { 26 | filter: drop-shadow(1px 1px 4px rgba(0, 0, 0, 0.6)); 27 | border: unset; 28 | } 29 | 30 | small { 31 | font-size: 0.775em; 32 | } 33 | </style> 34 | <script src="dist/frappe-gantt.umd.js"></script> 35 | 36 | 37 |

38 |

Frappe Gantt

39 |
40 |
41 |
42 |

Set edit access

43 |

44 | Easy make sure your employees change only what 45 | they need to. 46 |

47 |
48 | <input 49 | class="form-check-input" 50 | type="checkbox" 51 | role="switch" 52 | id="mutable-general" 53 | checked 54 | /> 55 | <label class="form-check-label" for="mutable-general" 56 | >Editable</label 57 | > 58 |
59 |
60 | <input 61 | class="form-check-input" 62 | type="checkbox" 63 | role="switch" 64 | id="mutable-progress" 65 | checked 66 | /> 67 | <label class="form-check-label" for="mutable-general" 68 | >Progress editable</label 69 | > 70 |
71 |
72 | <input 73 | class="form-check-input" 74 | type="checkbox" 75 | role="switch" 76 | id="mutable-dates" 77 | checked 78 | /> 79 | <label class="form-check-label" for="mutable-general" 80 | >Dates editable</label 81 | > 82 |
83 |
84 | 85 |
86 |
87 |
88 |
89 |
90 |

Versatile Actions

91 |

92 | Change the view mode, or scroll to today, or add 93 | anything you like β. 94 |

95 |
96 | <input 97 | class="form-check-input" 98 | type="checkbox" 99 | role="switch" 100 | id="toggle-today" 101 | checked 102 | /> 103 | <label class="form-check-label" for="mutable-general" 104 | >Scroll to Today</label 105 | > 106 |
107 |
108 | <input 109 | class="form-check-input" 110 | type="checkbox" 111 | role="switch" 112 | id="toggle-view-mode" 113 | checked 114 | /> 115 | <label class="form-check-label" for="mutable-general" 116 | >Change View Mode</label 117 | > 118 |
119 |
120 |
121 | 122 |
123 |
124 |

Mark Holidays

125 |

126 | Be it public holidays, company milestones, or just 127 | weekends, you can see it all. 128 |

129 |
130 | <input 131 | class="form-check-input" 132 | type="checkbox" 133 | role="switch" 134 | id="toggle-weekends" 135 | /> 136 | <label class="form-check-label" for="toggle-weekends" 137 | >Show weekends</label 138 | > 139 |
140 |
141 |
142 |
143 | 144 |
145 |
146 |

...or ignore them

147 |

148 | Remove time periods from your Gantt - they're now 149 | completely ignored. 150 |

151 |
152 | <input 153 | class="form-check-input" 154 | type="checkbox" 155 | role="switch" 156 | id="ignore-weekends" 157 | checked 158 | /> 159 | <label class="form-check-label" for="toggle-weekends" 160 | >Ignore weekends</label 161 | > 162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |

Control the styles completely.

170 | Modify Grid 171 |
172 | <label 173 | for="grid-height" 174 | class="form-label col-sm-5 col-form-label" 175 | >Grid Height:</label 176 | > 177 |
178 | <input 179 | id="grid-height" 180 | class="form-range align-items-end" 181 | type="range" 182 | min="150" 183 | max="600" 184 | value="300" 185 | /> 186 |
187 |
188 |
189 | <label 190 | for="padding" 191 | class="form-label col-sm-5 col-form-label" 192 | >Padding:</label 193 | > 194 |
195 | <input 196 | id="padding" 197 | class="form-range align-items-end" 198 | type="range" 199 | min="3" 200 | max="50" 201 | value="18" 202 | /> 203 |
204 |
205 |
206 | <label 207 | for="column-width" 208 | class="form-label col-sm-5 col-form-label" 209 | >Column Width:</label 210 | > 211 |
212 | <input 213 | id="column-width" 214 | class="form-range align-items-end" 215 | type="range" 216 | min="30" 217 | max="70" 218 | value="30" 219 | /> 220 |
221 |
222 |
223 | Modify Bar 224 |
225 |
226 | <label 227 | for="bar-height" 228 | class="form-label col-sm-5 col-form-label" 229 | >Height:</label 230 | > 231 |
232 | <input 233 | id="bar-height" 234 | class="form-range align-items-end" 235 | type="range" 236 | min="10" 237 | max="100" 238 | value="30" 239 | /> 240 |
241 |
242 |
243 | <label 244 | for="bar-radius" 245 | class="form-label col-sm-5 col-form-label" 246 | >Radius:</label 247 | > 248 |
249 | <input 250 | id="bar-radius" 251 | class="form-range align-items-end" 252 | type="range" 253 | min="1" 254 | max="50" 255 | value="3" 256 | /> 257 |
258 |
259 |
260 | <label 261 | for="arrow-curve" 262 | class="form-label col-sm-5 col-form-label" 263 | >Arrow curving:</label 264 | > 265 |
266 | <input 267 | id="arrow-curve" 268 | class="form-range align-items-end" 269 | type="range" 270 | min="1" 271 | max="50" 272 | value="3" 273 | /> 274 |
275 |
276 |
277 |
278 | 279 |
280 |
281 |

Frappe Gantt - for you.

282 |

283 | Insane levels of customizability - change anything, 284 | everything. 285 |

286 |
287 | Snap By: 288 | <input 289 | class="form-control" 290 | id="snap-at-qty" 291 | type="number" 292 | value="1" 293 | /> 294 | 295 | Second 296 | Minute 297 | Hour 298 | Day 299 | Month 300 | Year 301 | 302 |
303 |
304 | <label class="form-check-label" for="auto-move-label" 305 | >Toggle auto-moving label</label 306 | > 307 | <input 308 | class="form-check-input" 309 | type="checkbox" 310 | role="switch" 311 | id="auto-move-label" 312 | /> 313 |
314 |
315 |
316 |
317 |
318 | <script type="module"> 319 | const rawToday = new Date(); 320 | const today = 321 | Date.UTC( 322 | rawToday.getFullYear(), 323 | rawToday.getMonth(), 324 | rawToday.getDate(), 325 | ) + 326 | new Date().getTimezoneOffset() * 60000; 327 | 328 | function random(begin = 10, end = 90, multiple = 10) { 329 | let k; 330 | do { 331 | k = Math.floor(Math.random() * 100); 332 | } while (k < begin || k > end || k % multiple !== 0); 333 | return k; 334 | } 335 | 336 | const daysSince = (dx) => new Date(today + dx * 86400000); 337 | let tasks = [ 338 | { 339 | start: daysSince(-2), 340 | end: daysSince(2), 341 | name: 'Redesign website', 342 | id: 'Task 0', 343 | progress: random(), 344 | }, 345 | { 346 | start: daysSince(3), 347 | duration: '6d', 348 | name: 'Write new content', 349 | id: 'Task 1', 350 | progress: random(), 351 | important: true, 352 | dependencies: 'Task 0', 353 | }, 354 | { 355 | start: daysSince(4), 356 | duration: '2d', 357 | name: 'Apply new styles', 358 | id: 'Task 2', 359 | progress: random(), 360 | }, 361 | { 362 | start: daysSince(-4), 363 | end: daysSince(0), 364 | name: 'Review', 365 | id: 'Task 3', 366 | progress: random(), 367 | }, 368 | ]; 369 | 370 | const tasksSpread = [ 371 | { 372 | start: daysSince(-30), 373 | end: daysSince(-10), 374 | name: 'Redesign website', 375 | id: 'Task 0', 376 | progress: random(), 377 | }, 378 | { 379 | start: daysSince(-15), 380 | duration: '21d', 381 | name: 'Write new content', 382 | id: 'Task 1', 383 | progress: random(), 384 | important: true, 385 | }, 386 | { 387 | start: daysSince(10), 388 | duration: '14d', 389 | name: 'Review', 390 | id: 'Task 3', 391 | progress: random(), 392 | }, 393 | { 394 | start: daysSince(-3), 395 | duration: '4d', 396 | name: 'Publish', 397 | id: 'Task 4', 398 | progress: random(), 399 | }, 400 | ]; 401 | 402 | const tasksDependencies = [ 403 | { 404 | start: daysSince(-2), 405 | end: daysSince(2), 406 | name: 'Redesign website', 407 | id: 'Task 0', 408 | progress: random(), 409 | }, 410 | { 411 | start: daysSince(3), 412 | duration: '6d', 413 | name: 'Write new content', 414 | id: 'Task 1', 415 | progress: random(), 416 | dependencies: 'Task 0', 417 | important: true, 418 | }, 419 | { 420 | start: daysSince(4), 421 | duration: '2d', 422 | name: 'Apply new styles', 423 | id: 'Task 2', 424 | progress: random(), 425 | }, 426 | { 427 | start: daysSince(-4), 428 | end: daysSince(0), 429 | name: 'Review', 430 | id: 'Task 3', 431 | custom_class: 'readonly', 432 | progress: random(), 433 | }, 434 | ]; 435 | let tasksMany = [ 436 | { 437 | start: daysSince(-7), 438 | end: daysSince(-5), 439 | name: 'Initial brainstorming', 440 | id: 'Task 0', 441 | progress: random(), 442 | }, 443 | { 444 | start: daysSince(-3), 445 | end: daysSince(1), 446 | name: 'Develop wireframe', 447 | id: 'Task 1', 448 | progress: random(), 449 | dependencies: 'Task 0', 450 | }, 451 | { 452 | start: daysSince(-1), 453 | duration: '4d', 454 | name: 'Client meeting', 455 | id: 'Task 2', 456 | progress: random(), 457 | important: true, 458 | }, 459 | { 460 | start: daysSince(1), 461 | duration: '7d', 462 | name: 'Create prototype', 463 | id: 'Task 3', 464 | dependencies: 'Task 2', 465 | progress: random(), 466 | }, 467 | { 468 | start: daysSince(3), 469 | duration: '5d', 470 | name: 'Test design with users', 471 | dependencies: 'Task 2', 472 | id: 'Task 4', 473 | progress: random(), 474 | important: true, 475 | }, 476 | { 477 | start: daysSince(5), 478 | end: daysSince(10), 479 | name: 'Write technical documentation', 480 | id: 'Task 5', 481 | progress: random(), 482 | }, 483 | { 484 | start: daysSince(8), 485 | duration: '3d', 486 | name: 'Prepare demo', 487 | id: 'Task 6', 488 | progress: random(), 489 | }, 490 | { 491 | start: daysSince(10), 492 | end: daysSince(12), 493 | name: 'Final client review', 494 | id: 'Task 7', 495 | progress: random(), 496 | important: true, 497 | }, 498 | { 499 | start: daysSince(14), 500 | duration: '6d', 501 | name: 'Implement feedback', 502 | id: 'Task 8', 503 | progress: random(), 504 | }, 505 | { 506 | start: daysSince(16), 507 | duration: '4d', 508 | name: 'Launch website', 509 | id: 'Task 9', 510 | progress: random(), 511 | important: true, 512 | }, 513 | ]; 514 | 515 | const HOLIDAYS = [ 516 | { name: 'Republic Day', date: '2024-01-26' }, 517 | { name: 'Maha Shivratri', date: '2024-02-23' }, 518 | { name: 'Holi', date: '2024-03-11' }, 519 | { name: 'Mahavir Jayanthi', date: '2024-04-07' }, 520 | { name: 'Good Friday', date: '2024-04-10' }, 521 | { name: 'May Day', date: '2024-05-01' }, 522 | { name: 'Buddha Purnima', date: '2024-05-08' }, 523 | { name: 'Krishna Janmastami', date: '2024-08-14' }, 524 | { name: 'Independence Day', date: '2024-08-15' }, 525 | { name: 'Ganesh Chaturthi', date: '2024-08-23' }, 526 | { name: 'Id-Ul-Fitr', date: '2024-09-21' }, 527 | { name: 'Vijaya Dashami', date: '2024-09-28' }, 528 | { name: 'Mahatma Gandhi Jayanti', date: '2024-10-02' }, 529 | { name: 'Diwali', date: '2024-10-17' }, 530 | { name: 'Guru Nanak Jayanthi', date: '2024-11-02' }, 531 | { name: 'Christmas', date: '2024-12-25' }, 532 | ]; 533 | 534 | const mutablity = new Gantt('#mutability', tasks); 535 | const sideheader = new Gantt('#sideheader', tasksSpread, { 536 | today_button: true, 537 | view_mode_select: true, 538 | holidays: null, 539 | }); 540 | 541 | const holidays = new Gantt('#holidays', tasksSpread, { 542 | holidays: { 543 | '#bfdbfe': [], 544 | '#a3e635': HOLIDAYS, 545 | }, 546 | }); 547 | 548 | const ignore = new Gantt('#ignore', tasks, { 549 | ignore: ['weekend', ...HOLIDAYS.map((k) => k.date)], 550 | holidays: null, 551 | scroll_to: daysSince(-10), 552 | }); 553 | 554 | const styling = new Gantt('#styling', tasksMany, { 555 | holidays: null, 556 | scroll_to: daysSince(-10), 557 | }); 558 | 559 | const advanced = new Gantt('#advanced', tasksSpread, { 560 | holidays: null, 561 | view_mode_select: true, 562 | snap_at: '1d', 563 | auto_move_label: false, 564 | scroll_to: 'today', 565 | }); 566 | 567 | const UPDATES = [ 568 | [ 569 | mutablity, 570 | { 571 | 'mutable-general': 'opp__readonly', 572 | 'mutable-dates': 'opp__readonly_dates', 573 | 'mutable-progress': 'opp__readonly_progress', 574 | }, 575 | (id, val) => { 576 | if (id === 'mutable-general') { 577 | document.getElementById('mutable-dates').checked = 578 | !val; 579 | document.getElementById( 580 | 'mutable-progress', 581 | ).checked = !val; 582 | } 583 | }, 584 | ], 585 | [ 586 | sideheader, 587 | { 588 | 'toggle-today': 'today_button', 589 | 'toggle-view-mode': 'view_mode_select', 590 | }, 591 | ], 592 | [ 593 | holidays, 594 | { 595 | 'toggle-weekends': [ 596 | 'holidays', 597 | { '#a3e635': HOLIDAYS, '#bfdbfe': 'weekend' }, 598 | { '#a3e635': HOLIDAYS, '#bfdbfe': [] }, 599 | ], 600 | }, 601 | ], 602 | [ 603 | ignore, 604 | { 605 | 'ignore-weekends': ['ignore', ['weekend'], []], 606 | }, 607 | ], 608 | [ 609 | styling, 610 | { 611 | 'bar-radius': 'bar_corner_radius', 612 | 'bar-height': 'bar_height', 613 | 'arrow-curve': 'arrow_curve', 614 | 'column-width': 'column_width', 615 | 'grid-height': 'container_height', 616 | padding: 'padding', 617 | }, 618 | ], 619 | [ 620 | advanced, 621 | { 622 | 'auto-move-label': 'auto_move_label', 623 | 'snap-at-qty': (val) => ({ 624 | snap_at: 625 | val + 626 | document.getElementById('snap-at-scale').value, 627 | }), 628 | 'snap-at-scale': (val) => ({ 629 | snap_at: 630 | document.getElementById('snap-at-qty').value + 631 | val, 632 | }), 633 | }, 634 | ], 635 | ]; 636 | 637 | for (let [chart, details, after] of UPDATES) { 638 | for (let id in details) { 639 | let el = document.getElementById(id); 640 | el.onchange = (e) => { 641 | let label = details[id]; 642 | let val; 643 | 644 | if (e.currentTarget.type === 'checkbox') { 645 | if (typeof label === 'string') { 646 | let opposite = label.slice(0, 5) === 'opp__'; 647 | if (opposite) label = label.slice(5); 648 | val = opposite 649 | ? !e.currentTarget.checked 650 | : e.currentTarget.checked; 651 | } else { 652 | val = label[e.currentTarget.checked ? 1 : 2]; 653 | label = label[0]; 654 | } 655 | } else { 656 | val = +e.currentTarget.value; 657 | } 658 | 659 | let store = chart.options.scroll_to; 660 | let scroll = chart.$container.scrollLeft; 661 | if (typeof label === 'function') { 662 | chart.update_options({ 663 | ...label(val), 664 | scroll_to: null, 665 | }); 666 | } else { 667 | chart.update_options({ 668 | [label]: val, 669 | scroll_to: null, 670 | }); 671 | } 672 | 673 | chart.options.scroll_to = store; 674 | chart.$container.scrollLeft = scroll; 675 | after && after(id, val, chart); 676 | }; 677 | } 678 | } 679 | 680 | // const OPTIONS_UPDATE = [ 681 | // // [ 682 | // // styling, 683 | // // { 684 | // // 'bar-spacing': { 685 | // // bar_corner_radius: [ 686 | // // 'config', 687 | // // () => 688 | // // +document.getElementById('bar-radius') 689 | // // .value, 690 | // // , 691 | // // ], 692 | // // bar_height: [ 693 | // // 'config', 694 | // // () => 695 | // // +document.getElementById('bar-height') 696 | // // .value, 697 | // // ], 698 | // // arrow_curve: [ 699 | // // 'config', 700 | // // () => 701 | // // +document.getElementById('arrow-curve') 702 | // // .value, 703 | // // ], 704 | // // }, 705 | // // }, 706 | // // ], 707 | // [ 708 | // advanced, 709 | // { 710 | // 'snap-by': { 711 | // BEFORE: (chart) => chart.$container.scrollLeft, 712 | // snap_at: [ 713 | // 'config', 714 | // (scale) => { 715 | // return ( 716 | // document.getElementById('snap-at-qty') 717 | // .value + 718 | // document.getElementById('snap-at-scale') 719 | // .value 720 | // ); 721 | // }, 722 | // ], 723 | // view_mode: ['config', (k) => k], 724 | // scroll_to: ['config', () => false], 725 | // AFTER: (before, chart) => 726 | // (chart.$container.scrollLeft = before), 727 | // }, 728 | // 'auto-move-label': { 729 | // BEFORE: (chart) => 730 | // chart.change_view_mode('Day') || 731 | // chart.$container.scrollLeft, 732 | // view_mode: ['config', (k) => k], 733 | // auto_move_label: 'opp', 734 | // scroll_to: ['config', () => false], 735 | // AFTER: (before, chart) => 736 | // (chart.$container.scrollLeft = before), 737 | // }, 738 | // }, 739 | // ], 740 | // ]; 741 | 742 | // for (let [chart, options] of OPTIONS_UPDATE) { 743 | // for (let id in options) { 744 | // let el = document.getElementById(id); 745 | // el.onclick = () => { 746 | // const before = 747 | // options[id].BEFORE && options[id].BEFORE(chart); 748 | // let newOptions = {}; 749 | // for (let k in options[id]) { 750 | // if (k === 'AFTER' || k === 'BEFORE') continue; 751 | // if (options[id][k] === 'opp') { 752 | // newOptions[k] = !chart.options[k]; 753 | // if (chart.options[k]) { 754 | // el.innerHTML = el.innerHTML.replace( 755 | // 'Hide', 756 | // 'Show', 757 | // ); 758 | // } else { 759 | // el.innerHTML = el.innerHTML.replace( 760 | // 'Show', 761 | // 'Hide', 762 | // ); 763 | // } 764 | // } else if (options[id][k][0] === 'config') { 765 | // newOptions[k] = options[id][k][1]( 766 | // chart.options[k], 767 | // chart, 768 | // ); 769 | // } else { 770 | // newOptions[k] = options[id][k]; 771 | // } 772 | // } 773 | // chart.update_options(newOptions); 774 | // options[id].AFTER && options[id].AFTER(before, chart); 775 | // }; 776 | // } 777 | // } 778 | </script> 779 | <script 780 | src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" 781 | integrity_no="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" 782 | crossorigin="anonymous" 783 | ></script> 784 | 785 | 786 |


/license.txt:

1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Frappe Technologies Pvt. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


/package.json:

1 | { 2 | "name": "frappe-gantt", 3 | "version": "1.0.3", 4 | "description": "A simple, modern, interactive gantt library for the web", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build-dev": "vite build --watch", 10 | "build": "vite build", 11 | "lint": "eslint src/**/.js", 12 | "prettier": "prettier --write "{src/,tests/,rollup.config}.js"", 13 | "prettier-check": "prettier --check "{src/,tests/,rollup.config}.js"" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/frappe/gantt.git" 18 | }, 19 | "files": [ 20 | "src", 21 | "dist", 22 | "README.md" 23 | ], 24 | "exports": { 25 | ".": { 26 | "require": "./dist/frappe-gantt.umd.js", 27 | "import": "./dist/frappe-gantt.es.js", 28 | "style": "./dist/frappe-gantt.css" 29 | } 30 | }, 31 | "keywords": [ 32 | "gantt", 33 | "svg", 34 | "simple gantt", 35 | "project timeline", 36 | "interactive gantt", 37 | "project management" 38 | ], 39 | "author": "Faris Ansari", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/frappe/gantt/issues" 43 | }, 44 | "homepage": "https://github.com/frappe/gantt", 45 | "devDependencies": { 46 | "eslint": "^9.15.0", 47 | "eslint-config-prettier": "^2.9.0", 48 | "eslint-plugin-prettier": "^2.6.0", 49 | "postcss-nesting": "^12.1.2", 50 | "prettier": "3.2.5", 51 | "vite": "^5.2.10" 52 | }, 53 | "eslintIgnore": [ 54 | "dist" 55 | ], 56 | "sideEffects": [ 57 | ".css" 58 | ] 59 | } 60 |


/pnpm-lock.yaml:

1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | eslint: 12 | specifier: ^9.15.0 13 | version: 9.15.0 14 | eslint-config-prettier: 15 | specifier: ^2.9.0 16 | version: 2.10.0([email protected]) 17 | eslint-plugin-prettier: 18 | specifier: ^2.6.0 19 | version: 2.7.0([email protected]) 20 | postcss-nesting: 21 | specifier: ^12.1.2 22 | version: 12.1.5([email protected]) 23 | prettier: 24 | specifier: 3.2.5 25 | version: 3.2.5 26 | vite: 27 | specifier: ^5.2.10 28 | version: 5.4.12(@types/[email protected]) 29 | 30 | packages: 31 | 32 | '@csstools/[email protected]': 33 | resolution: {integrity: sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==} 34 | engines: {node: ^14 || ^16 || >=18} 35 | peerDependencies: 36 | postcss-selector-parser: ^6.0.13 37 | 38 | '@csstools/[email protected]': 39 | resolution: {integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==} 40 | engines: {node: ^14 || ^16 || >=18} 41 | peerDependencies: 42 | postcss-selector-parser: ^6.0.13 43 | 44 | '@esbuild/[email protected]': 45 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 46 | engines: {node: '>=12'} 47 | cpu: [ppc64] 48 | os: [aix] 49 | 50 | '@esbuild/[email protected]': 51 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 52 | engines: {node: '>=12'} 53 | cpu: [arm64] 54 | os: [android] 55 | 56 | '@esbuild/[email protected]': 57 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 58 | engines: {node: '>=12'} 59 | cpu: [arm] 60 | os: [android] 61 | 62 | '@esbuild/[email protected]': 63 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 64 | engines: {node: '>=12'} 65 | cpu: [x64] 66 | os: [android] 67 | 68 | '@esbuild/[email protected]': 69 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 70 | engines: {node: '>=12'} 71 | cpu: [arm64] 72 | os: [darwin] 73 | 74 | '@esbuild/[email protected]': 75 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 76 | engines: {node: '>=12'} 77 | cpu: [x64] 78 | os: [darwin] 79 | 80 | '@esbuild/[email protected]': 81 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 82 | engines: {node: '>=12'} 83 | cpu: [arm64] 84 | os: [freebsd] 85 | 86 | '@esbuild/[email protected]': 87 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 88 | engines: {node: '>=12'} 89 | cpu: [x64] 90 | os: [freebsd] 91 | 92 | '@esbuild/[email protected]': 93 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 94 | engines: {node: '>=12'} 95 | cpu: [arm64] 96 | os: [linux] 97 | 98 | '@esbuild/[email protected]': 99 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 100 | engines: {node: '>=12'} 101 | cpu: [arm] 102 | os: [linux] 103 | 104 | '@esbuild/[email protected]': 105 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 106 | engines: {node: '>=12'} 107 | cpu: [ia32] 108 | os: [linux] 109 | 110 | '@esbuild/[email protected]': 111 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 112 | engines: {node: '>=12'} 113 | cpu: [loong64] 114 | os: [linux] 115 | 116 | '@esbuild/[email protected]': 117 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 118 | engines: {node: '>=12'} 119 | cpu: [mips64el] 120 | os: [linux] 121 | 122 | '@esbuild/[email protected]': 123 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 124 | engines: {node: '>=12'} 125 | cpu: [ppc64] 126 | os: [linux] 127 | 128 | '@esbuild/[email protected]': 129 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 130 | engines: {node: '>=12'} 131 | cpu: [riscv64] 132 | os: [linux] 133 | 134 | '@esbuild/[email protected]': 135 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 136 | engines: {node: '>=12'} 137 | cpu: [s390x] 138 | os: [linux] 139 | 140 | '@esbuild/[email protected]': 141 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 142 | engines: {node: '>=12'} 143 | cpu: [x64] 144 | os: [linux] 145 | 146 | '@esbuild/[email protected]': 147 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 148 | engines: {node: '>=12'} 149 | cpu: [x64] 150 | os: [netbsd] 151 | 152 | '@esbuild/[email protected]': 153 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 154 | engines: {node: '>=12'} 155 | cpu: [x64] 156 | os: [openbsd] 157 | 158 | '@esbuild/[email protected]': 159 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 160 | engines: {node: '>=12'} 161 | cpu: [x64] 162 | os: [sunos] 163 | 164 | '@esbuild/[email protected]': 165 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 166 | engines: {node: '>=12'} 167 | cpu: [arm64] 168 | os: [win32] 169 | 170 | '@esbuild/[email protected]': 171 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 172 | engines: {node: '>=12'} 173 | cpu: [ia32] 174 | os: [win32] 175 | 176 | '@esbuild/[email protected]': 177 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 178 | engines: {node: '>=12'} 179 | cpu: [x64] 180 | os: [win32] 181 | 182 | '@eslint-community/[email protected]': 183 | resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} 184 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 185 | peerDependencies: 186 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 187 | 188 | '@eslint-community/[email protected]': 189 | resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} 190 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 191 | 192 | '@eslint/[email protected]': 193 | resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} 194 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 195 | 196 | '@eslint/[email protected]': 197 | resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} 198 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 199 | 200 | '@eslint/[email protected]': 201 | resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} 202 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 203 | 204 | '@eslint/[email protected]': 205 | resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} 206 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 207 | 208 | '@eslint/[email protected]': 209 | resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} 210 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 211 | 212 | '@eslint/[email protected]': 213 | resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} 214 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 215 | 216 | '@humanfs/[email protected]': 217 | resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} 218 | engines: {node: '>=18.18.0'} 219 | 220 | '@humanfs/[email protected]': 221 | resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} 222 | engines: {node: '>=18.18.0'} 223 | 224 | '@humanwhocodes/[email protected]': 225 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 226 | engines: {node: '>=12.22'} 227 | 228 | '@humanwhocodes/[email protected]': 229 | resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} 230 | engines: {node: '>=18.18'} 231 | 232 | '@humanwhocodes/[email protected]': 233 | resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} 234 | engines: {node: '>=18.18'} 235 | 236 | '@rollup/[email protected]': 237 | resolution: {integrity: sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==} 238 | cpu: [arm] 239 | os: [android] 240 | 241 | '@rollup/[email protected]': 242 | resolution: {integrity: sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==} 243 | cpu: [arm64] 244 | os: [android] 245 | 246 | '@rollup/[email protected]': 247 | resolution: {integrity: sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==} 248 | cpu: [arm64] 249 | os: [darwin] 250 | 251 | '@rollup/[email protected]': 252 | resolution: {integrity: sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==} 253 | cpu: [x64] 254 | os: [darwin] 255 | 256 | '@rollup/[email protected]': 257 | resolution: {integrity: sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==} 258 | cpu: [arm64] 259 | os: [freebsd] 260 | 261 | '@rollup/[email protected]': 262 | resolution: {integrity: sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==} 263 | cpu: [x64] 264 | os: [freebsd] 265 | 266 | '@rollup/[email protected]': 267 | resolution: {integrity: sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==} 268 | cpu: [arm] 269 | os: [linux] 270 | 271 | '@rollup/[email protected]': 272 | resolution: {integrity: sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==} 273 | cpu: [arm] 274 | os: [linux] 275 | 276 | '@rollup/[email protected]': 277 | resolution: {integrity: sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==} 278 | cpu: [arm64] 279 | os: [linux] 280 | 281 | '@rollup/[email protected]': 282 | resolution: {integrity: sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==} 283 | cpu: [arm64] 284 | os: [linux] 285 | 286 | '@rollup/[email protected]': 287 | resolution: {integrity: sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==} 288 | cpu: [loong64] 289 | os: [linux] 290 | 291 | '@rollup/[email protected]': 292 | resolution: {integrity: sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==} 293 | cpu: [ppc64] 294 | os: [linux] 295 | 296 | '@rollup/[email protected]': 297 | resolution: {integrity: sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==} 298 | cpu: [riscv64] 299 | os: [linux] 300 | 301 | '@rollup/[email protected]': 302 | resolution: {integrity: sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==} 303 | cpu: [s390x] 304 | os: [linux] 305 | 306 | '@rollup/[email protected]': 307 | resolution: {integrity: sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==} 308 | cpu: [x64] 309 | os: [linux] 310 | 311 | '@rollup/[email protected]': 312 | resolution: {integrity: sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==} 313 | cpu: [x64] 314 | os: [linux] 315 | 316 | '@rollup/[email protected]': 317 | resolution: {integrity: sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==} 318 | cpu: [arm64] 319 | os: [win32] 320 | 321 | '@rollup/[email protected]': 322 | resolution: {integrity: sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==} 323 | cpu: [ia32] 324 | os: [win32] 325 | 326 | '@rollup/[email protected]': 327 | resolution: {integrity: sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==} 328 | cpu: [x64] 329 | os: [win32] 330 | 331 | '@types/[email protected]': 332 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 333 | 334 | '@types/[email protected]': 335 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 336 | 337 | '@types/[email protected]': 338 | resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} 339 | 340 | [email protected]: 341 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 342 | peerDependencies: 343 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 344 | 345 | [email protected]: 346 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 347 | engines: {node: '>=0.4.0'} 348 | hasBin: true 349 | 350 | [email protected]: 351 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 352 | 353 | [email protected]: 354 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 355 | engines: {node: '>=8'} 356 | 357 | [email protected]: 358 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 359 | 360 | [email protected]: 361 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 362 | 363 | [email protected]: 364 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 365 | 366 | [email protected]: 367 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 368 | engines: {node: '>=6'} 369 | 370 | [email protected]: 371 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 372 | engines: {node: '>=10'} 373 | 374 | [email protected]: 375 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 376 | engines: {node: '>=7.0.0'} 377 | 378 | [email protected]: 379 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 380 | 381 | [email protected]: 382 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 383 | 384 | [email protected]: 385 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 386 | engines: {node: '>= 8'} 387 | 388 | [email protected]: 389 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 390 | engines: {node: '>=4'} 391 | hasBin: true 392 | 393 | [email protected]: 394 | resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} 395 | engines: {node: '>=6.0'} 396 | peerDependencies: 397 | supports-color: '' 398 | peerDependenciesMeta: 399 | supports-color: 400 | optional: true 401 | 402 | [email protected]: 403 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 404 | 405 | [email protected]: 406 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 407 | engines: {node: '>=12'} 408 | hasBin: true 409 | 410 | [email protected]: 411 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 412 | engines: {node: '>=10'} 413 | 414 | [email protected]: 415 | resolution: {integrity: sha512-Mhl90VLucfBuhmcWBgbUNtgBiK955iCDK1+aHAz7QfDQF6wuzWZ6JjihZ3ejJoGlJWIuko7xLqNm8BA5uenKhA==} 416 | hasBin: true 417 | peerDependencies: 418 | eslint: '>=3.14.1' 419 | 420 | [email protected]: 421 | resolution: {integrity: sha512-CStQYJgALoQBw3FsBzH0VOVDRnJ/ZimUlpLm226U8qgqYJfPOY/CPK6wyRInMxh73HSKg5wyRwdS4BVYYHwokA==} 422 | engines: {node: '>=4.0.0'} 423 | peerDependencies: 424 | prettier: '>= 0.11.0' 425 | 426 | [email protected]: 427 | resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} 428 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 429 | 430 | [email protected]: 431 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 432 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 433 | 434 | [email protected]: 435 | resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} 436 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 437 | 438 | [email protected]: 439 | resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} 440 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 441 | hasBin: true 442 | peerDependencies: 443 | jiti: '' 444 | peerDependenciesMeta: 445 | jiti: 446 | optional: true 447 | 448 | [email protected]: 449 | resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} 450 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 451 | 452 | [email protected]: 453 | resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 454 | engines: {node: '>=0.10'} 455 | 456 | [email protected]: 457 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 458 | engines: {node: '>=4.0'} 459 | 460 | [email protected]: 461 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 462 | engines: {node: '>=4.0'} 463 | 464 | [email protected]: 465 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 466 | engines: {node: '>=0.10.0'} 467 | 468 | [email protected]: 469 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 470 | 471 | [email protected]: 472 | resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} 473 | 474 | [email protected]: 475 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 476 | 477 | [email protected]: 478 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 479 | 480 | [email protected]: 481 | resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 482 | engines: {node: '>=16.0.0'} 483 | 484 | [email protected]: 485 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 486 | engines: {node: '>=10'} 487 | 488 | [email protected]: 489 | resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 490 | engines: {node: '>=16'} 491 | 492 | [email protected]: 493 | resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} 494 | 495 | [email protected]: 496 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 497 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 498 | os: [darwin] 499 | 500 | [email protected]: 501 | resolution: {integrity: sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA==} 502 | engines: {node: '>=0.12.0'} 503 | 504 | [email protected]: 505 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 506 | engines: {node: '>=10.13.0'} 507 | 508 | [email protected]: 509 | resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 510 | engines: {node: '>=18'} 511 | 512 | [email protected]: 513 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 514 | engines: {node: '>=8'} 515 | 516 | [email protected]: 517 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 518 | engines: {node: '>= 4'} 519 | 520 | [email protected]: 521 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 522 | engines: {node: '>=6'} 523 | 524 | [email protected]: 525 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 526 | engines: {node: '>=0.8.19'} 527 | 528 | [email protected]: 529 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 530 | engines: {node: '>=0.10.0'} 531 | 532 | [email protected]: 533 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 534 | engines: {node: '>=0.10.0'} 535 | 536 | [email protected]: 537 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 538 | 539 | [email protected]: 540 | resolution: {integrity: sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==} 541 | 542 | [email protected]: 543 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 544 | hasBin: true 545 | 546 | [email protected]: 547 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 548 | 549 | [email protected]: 550 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 551 | 552 | [email protected]: 553 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 554 | 555 | [email protected]: 556 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 557 | 558 | [email protected]: 559 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 560 | engines: {node: '>= 0.8.0'} 561 | 562 | [email protected]: 563 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 564 | engines: {node: '>=10'} 565 | 566 | [email protected]: 567 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 568 | 569 | [email protected]: 570 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 571 | 572 | [email protected]: 573 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 574 | 575 | [email protected]: 576 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 577 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 578 | hasBin: true 579 | 580 | [email protected]: 581 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 582 | 583 | [email protected]: 584 | resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 585 | engines: {node: '>= 0.8.0'} 586 | 587 | [email protected]: 588 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 589 | engines: {node: '>=10'} 590 | 591 | [email protected]: 592 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 593 | engines: {node: '>=10'} 594 | 595 | [email protected]: 596 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 597 | engines: {node: '>=6'} 598 | 599 | [email protected]: 600 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 601 | engines: {node: '>=8'} 602 | 603 | [email protected]: 604 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 605 | engines: {node: '>=8'} 606 | 607 | [email protected]: 608 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 609 | 610 | [email protected]: 611 | resolution: {integrity: sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==} 612 | engines: {node: ^14 || ^16 || >=18} 613 | peerDependencies: 614 | postcss: ^8.4 615 | 616 | [email protected]: 617 | resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} 618 | engines: {node: '>=4'} 619 | 620 | [email protected]: 621 | resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} 622 | engines: {node: ^10 || ^12 || >=14} 623 | 624 | [email protected]: 625 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 626 | engines: {node: '>= 0.8.0'} 627 | 628 | [email protected]: 629 | resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} 630 | engines: {node: '>=14'} 631 | hasBin: true 632 | 633 | [email protected]: 634 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 635 | engines: {node: '>=6'} 636 | 637 | [email protected]: 638 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 639 | engines: {node: '>=4'} 640 | 641 | [email protected]: 642 | resolution: {integrity: sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==} 643 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 644 | hasBin: true 645 | 646 | [email protected]: 647 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 648 | engines: {node: '>=8'} 649 | 650 | [email protected]: 651 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 652 | engines: {node: '>=8'} 653 | 654 | [email protected]: 655 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 656 | engines: {node: '>=0.10.0'} 657 | 658 | [email protected]: 659 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 660 | engines: {node: '>=8'} 661 | 662 | [email protected]: 663 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 664 | engines: {node: '>=8'} 665 | 666 | [email protected]: 667 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 668 | engines: {node: '>= 0.8.0'} 669 | 670 | [email protected]: 671 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 672 | 673 | [email protected]: 674 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 675 | 676 | [email protected]: 677 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 678 | 679 | [email protected]: 680 | resolution: {integrity: sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==} 681 | engines: {node: ^18.0.0 || >=20.0.0} 682 | hasBin: true 683 | peerDependencies: 684 | '@types/node': ^18.0.0 || >=20.0.0 685 | less: '' 686 | lightningcss: ^1.21.0 687 | sass: '' 688 | sass-embedded: '' 689 | stylus: '' 690 | sugarss: '*' 691 | terser: ^5.4.0 692 | peerDependenciesMeta: 693 | '@types/node': 694 | optional: true 695 | less: 696 | optional: true 697 | lightningcss: 698 | optional: true 699 | sass: 700 | optional: true 701 | sass-embedded: 702 | optional: true 703 | stylus: 704 | optional: true 705 | sugarss: 706 | optional: true 707 | terser: 708 | optional: true 709 | 710 | [email protected]: 711 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 712 | engines: {node: '>= 8'} 713 | hasBin: true 714 | 715 | [email protected]: 716 | resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 717 | engines: {node: '>=0.10.0'} 718 | 719 | [email protected]: 720 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 721 | engines: {node: '>=10'} 722 | 723 | snapshots: 724 | 725 | '@csstools/[email protected]([email protected])': 726 | dependencies: 727 | postcss-selector-parser: 6.1.2 728 | 729 | '@csstools/[email protected]([email protected])': 730 | dependencies: 731 | postcss-selector-parser: 6.1.2 732 | 733 | '@esbuild/[email protected]': 734 | optional: true 735 | 736 | '@esbuild/[email protected]': 737 | optional: true 738 | 739 | '@esbuild/[email protected]': 740 | optional: true 741 | 742 | '@esbuild/[email protected]': 743 | optional: true 744 | 745 | '@esbuild/[email protected]': 746 | optional: true 747 | 748 | '@esbuild/[email protected]': 749 | optional: true 750 | 751 | '@esbuild/[email protected]': 752 | optional: true 753 | 754 | '@esbuild/[email protected]': 755 | optional: true 756 | 757 | '@esbuild/[email protected]': 758 | optional: true 759 | 760 | '@esbuild/[email protected]': 761 | optional: true 762 | 763 | '@esbuild/[email protected]': 764 | optional: true 765 | 766 | '@esbuild/[email protected]': 767 | optional: true 768 | 769 | '@esbuild/[email protected]': 770 | optional: true 771 | 772 | '@esbuild/[email protected]': 773 | optional: true 774 | 775 | '@esbuild/[email protected]': 776 | optional: true 777 | 778 | '@esbuild/[email protected]': 779 | optional: true 780 | 781 | '@esbuild/[email protected]': 782 | optional: true 783 | 784 | '@esbuild/[email protected]': 785 | optional: true 786 | 787 | '@esbuild/[email protected]': 788 | optional: true 789 | 790 | '@esbuild/[email protected]': 791 | optional: true 792 | 793 | '@esbuild/[email protected]': 794 | optional: true 795 | 796 | '@esbuild/[email protected]': 797 | optional: true 798 | 799 | '@esbuild/[email protected]': 800 | optional: true 801 | 802 | '@eslint-community/[email protected]([email protected])': 803 | dependencies: 804 | eslint: 9.15.0 805 | eslint-visitor-keys: 3.4.3 806 | 807 | '@eslint-community/[email protected]': {} 808 | 809 | '@eslint/[email protected]': 810 | dependencies: 811 | '@eslint/object-schema': 2.1.4 812 | debug: 4.3.7 813 | minimatch: 3.1.2 814 | transitivePeerDependencies: 815 | - supports-color 816 | 817 | '@eslint/[email protected]': {} 818 | 819 | '@eslint/[email protected]': 820 | dependencies: 821 | ajv: 6.12.6 822 | debug: 4.3.7 823 | espree: 10.3.0 824 | globals: 14.0.0 825 | ignore: 5.3.2 826 | import-fresh: 3.3.0 827 | js-yaml: 4.1.0 828 | minimatch: 3.1.2 829 | strip-json-comments: 3.1.1 830 | transitivePeerDependencies: 831 | - supports-color 832 | 833 | '@eslint/[email protected]': {} 834 | 835 | '@eslint/[email protected]': {} 836 | 837 | '@eslint/[email protected]': 838 | dependencies: 839 | levn: 0.4.1 840 | 841 | '@humanfs/[email protected]': {} 842 | 843 | '@humanfs/[email protected]': 844 | dependencies: 845 | '@humanfs/core': 0.19.1 846 | '@humanwhocodes/retry': 0.3.1 847 | 848 | '@humanwhocodes/[email protected]': {} 849 | 850 | '@humanwhocodes/[email protected]': {} 851 | 852 | '@humanwhocodes/[email protected]': {} 853 | 854 | '@rollup/[email protected]': 855 | optional: true 856 | 857 | '@rollup/[email protected]': 858 | optional: true 859 | 860 | '@rollup/[email protected]': 861 | optional: true 862 | 863 | '@rollup/[email protected]': 864 | optional: true 865 | 866 | '@rollup/[email protected]': 867 | optional: true 868 | 869 | '@rollup/[email protected]': 870 | optional: true 871 | 872 | '@rollup/[email protected]': 873 | optional: true 874 | 875 | '@rollup/[email protected]': 876 | optional: true 877 | 878 | '@rollup/[email protected]': 879 | optional: true 880 | 881 | '@rollup/[email protected]': 882 | optional: true 883 | 884 | '@rollup/[email protected]': 885 | optional: true 886 | 887 | '@rollup/[email protected]': 888 | optional: true 889 | 890 | '@rollup/[email protected]': 891 | optional: true 892 | 893 | '@rollup/[email protected]': 894 | optional: true 895 | 896 | '@rollup/[email protected]': 897 | optional: true 898 | 899 | '@rollup/[email protected]': 900 | optional: true 901 | 902 | '@rollup/[email protected]': 903 | optional: true 904 | 905 | '@rollup/[email protected]': 906 | optional: true 907 | 908 | '@rollup/[email protected]': 909 | optional: true 910 | 911 | '@types/[email protected]': {} 912 | 913 | '@types/[email protected]': {} 914 | 915 | '@types/[email protected]': 916 | dependencies: 917 | undici-types: 6.20.0 918 | optional: true 919 | 920 | [email protected]([email protected]): 921 | dependencies: 922 | acorn: 8.14.0 923 | 924 | [email protected]: {} 925 | 926 | [email protected]: 927 | dependencies: 928 | fast-deep-equal: 3.1.3 929 | fast-json-stable-stringify: 2.1.0 930 | json-schema-traverse: 0.4.1 931 | uri-js: 4.4.1 932 | 933 | [email protected]: 934 | dependencies: 935 | color-convert: 2.0.1 936 | 937 | [email protected]: {} 938 | 939 | [email protected]: {} 940 | 941 | [email protected]: 942 | dependencies: 943 | balanced-match: 1.0.2 944 | concat-map: 0.0.1 945 | 946 | [email protected]: {} 947 | 948 | [email protected]: 949 | dependencies: 950 | ansi-styles: 4.3.0 951 | supports-color: 7.2.0 952 | 953 | [email protected]: 954 | dependencies: 955 | color-name: 1.1.4 956 | 957 | [email protected]: {} 958 | 959 | [email protected]: {} 960 | 961 | [email protected]: 962 | dependencies: 963 | path-key: 3.1.1 964 | shebang-command: 2.0.0 965 | which: 2.0.2 966 | 967 | [email protected]: {} 968 | 969 | [email protected]: 970 | dependencies: 971 | ms: 2.1.3 972 | 973 | [email protected]: {} 974 | 975 | [email protected]: 976 | optionalDependencies: 977 | '@esbuild/aix-ppc64': 0.21.5 978 | '@esbuild/android-arm': 0.21.5 979 | '@esbuild/android-arm64': 0.21.5 980 | '@esbuild/android-x64': 0.21.5 981 | '@esbuild/darwin-arm64': 0.21.5 982 | '@esbuild/darwin-x64': 0.21.5 983 | '@esbuild/freebsd-arm64': 0.21.5 984 | '@esbuild/freebsd-x64': 0.21.5 985 | '@esbuild/linux-arm': 0.21.5 986 | '@esbuild/linux-arm64': 0.21.5 987 | '@esbuild/linux-ia32': 0.21.5 988 | '@esbuild/linux-loong64': 0.21.5 989 | '@esbuild/linux-mips64el': 0.21.5 990 | '@esbuild/linux-ppc64': 0.21.5 991 | '@esbuild/linux-riscv64': 0.21.5 992 | '@esbuild/linux-s390x': 0.21.5 993 | '@esbuild/linux-x64': 0.21.5 994 | '@esbuild/netbsd-x64': 0.21.5 995 | '@esbuild/openbsd-x64': 0.21.5 996 | '@esbuild/sunos-x64': 0.21.5 997 | '@esbuild/win32-arm64': 0.21.5 998 | '@esbuild/win32-ia32': 0.21.5 999 | '@esbuild/win32-x64': 0.21.5 1000 | 1001 | [email protected]: {} 1002 | 1003 | [email protected]([email protected]): 1004 | dependencies: 1005 | eslint: 9.15.0 1006 | get-stdin: 5.0.1 1007 | 1008 | [email protected]([email protected]): 1009 | dependencies: 1010 | fast-diff: 1.3.0 1011 | jest-docblock: 21.2.0 1012 | prettier: 3.2.5 1013 | 1014 | [email protected]: 1015 | dependencies: 1016 | esrecurse: 4.3.0 1017 | estraverse: 5.3.0 1018 | 1019 | [email protected]: {} 1020 | 1021 | [email protected]: {} 1022 | 1023 | [email protected]: 1024 | dependencies: 1025 | '@eslint-community/eslint-utils': 4.4.1([email protected]) 1026 | '@eslint-community/regexpp': 4.12.1 1027 | '@eslint/config-array': 0.19.0 1028 | '@eslint/core': 0.9.0 1029 | '@eslint/eslintrc': 3.2.0 1030 | '@eslint/js': 9.15.0 1031 | '@eslint/plugin-kit': 0.2.3 1032 | '@humanfs/node': 0.16.6 1033 | '@humanwhocodes/module-importer': 1.0.1 1034 | '@humanwhocodes/retry': 0.4.1 1035 | '@types/estree': 1.0.6 1036 | '@types/json-schema': 7.0.15 1037 | ajv: 6.12.6 1038 | chalk: 4.1.2 1039 | cross-spawn: 7.0.6 1040 | debug: 4.3.7 1041 | escape-string-regexp: 4.0.0 1042 | eslint-scope: 8.2.0 1043 | eslint-visitor-keys: 4.2.0 1044 | espree: 10.3.0 1045 | esquery: 1.6.0 1046 | esutils: 2.0.3 1047 | fast-deep-equal: 3.1.3 1048 | file-entry-cache: 8.0.0 1049 | find-up: 5.0.0 1050 | glob-parent: 6.0.2 1051 | ignore: 5.3.2 1052 | imurmurhash: 0.1.4 1053 | is-glob: 4.0.3 1054 | json-stable-stringify-without-jsonify: 1.0.1 1055 | lodash.merge: 4.6.2 1056 | minimatch: 3.1.2 1057 | natural-compare: 1.4.0 1058 | optionator: 0.9.4 1059 | transitivePeerDependencies: 1060 | - supports-color 1061 | 1062 | [email protected]: 1063 | dependencies: 1064 | acorn: 8.14.0 1065 | acorn-jsx: 5.3.2([email protected]) 1066 | eslint-visitor-keys: 4.2.0 1067 | 1068 | [email protected]: 1069 | dependencies: 1070 | estraverse: 5.3.0 1071 | 1072 | [email protected]: 1073 | dependencies: 1074 | estraverse: 5.3.0 1075 | 1076 | [email protected]: {} 1077 | 1078 | [email protected]: {} 1079 | 1080 | [email protected]: {} 1081 | 1082 | [email protected]: {} 1083 | 1084 | [email protected]: {} 1085 | 1086 | [email protected]: {} 1087 | 1088 | [email protected]: 1089 | dependencies: 1090 | flat-cache: 4.0.1 1091 | 1092 | [email protected]: 1093 | dependencies: 1094 | locate-path: 6.0.0 1095 | path-exists: 4.0.0 1096 | 1097 | [email protected]: 1098 | dependencies: 1099 | flatted: 3.3.2 1100 | keyv: 4.5.4 1101 | 1102 | [email protected]: {} 1103 | 1104 | [email protected]: 1105 | optional: true 1106 | 1107 | [email protected]: {} 1108 | 1109 | [email protected]: 1110 | dependencies: 1111 | is-glob: 4.0.3 1112 | 1113 | [email protected]: {} 1114 | 1115 | [email protected]: {} 1116 | 1117 | [email protected]: {} 1118 | 1119 | [email protected]: 1120 | dependencies: 1121 | parent-module: 1.0.1 1122 | resolve-from: 4.0.0 1123 | 1124 | [email protected]: {} 1125 | 1126 | [email protected]: {} 1127 | 1128 | [email protected]: 1129 | dependencies: 1130 | is-extglob: 2.1.1 1131 | 1132 | [email protected]: {} 1133 | 1134 | [email protected]: {} 1135 | 1136 | [email protected]: 1137 | dependencies: 1138 | argparse: 2.0.1 1139 | 1140 | [email protected]: {} 1141 | 1142 | [email protected]: {} 1143 | 1144 | [email protected]: {} 1145 | 1146 | [email protected]: 1147 | dependencies: 1148 | json-buffer: 3.0.1 1149 | 1150 | [email protected]: 1151 | dependencies: 1152 | prelude-ls: 1.2.1 1153 | type-check: 0.4.0 1154 | 1155 | [email protected]: 1156 | dependencies: 1157 | p-locate: 5.0.0 1158 | 1159 | [email protected]: {} 1160 | 1161 | [email protected]: 1162 | dependencies: 1163 | brace-expansion: 1.1.11 1164 | 1165 | [email protected]: {} 1166 | 1167 | [email protected]: {} 1168 | 1169 | [email protected]: {} 1170 | 1171 | [email protected]: 1172 | dependencies: 1173 | deep-is: 0.1.4 1174 | fast-levenshtein: 2.0.6 1175 | levn: 0.4.1 1176 | prelude-ls: 1.2.1 1177 | type-check: 0.4.0 1178 | word-wrap: 1.2.5 1179 | 1180 | [email protected]: 1181 | dependencies: 1182 | yocto-queue: 0.1.0 1183 | 1184 | [email protected]: 1185 | dependencies: 1186 | p-limit: 3.1.0 1187 | 1188 | [email protected]: 1189 | dependencies: 1190 | callsites: 3.1.0 1191 | 1192 | [email protected]: {} 1193 | 1194 | [email protected]: {} 1195 | 1196 | [email protected]: {} 1197 | 1198 | [email protected]([email protected]): 1199 | dependencies: 1200 | '@csstools/selector-resolve-nested': 1.1.0([email protected]) 1201 | '@csstools/selector-specificity': 3.1.1([email protected]) 1202 | postcss: 8.5.1 1203 | postcss-selector-parser: 6.1.2 1204 | 1205 | [email protected]: 1206 | dependencies: 1207 | cssesc: 3.0.0 1208 | util-deprecate: 1.0.2 1209 | 1210 | [email protected]: 1211 | dependencies: 1212 | nanoid: 3.3.8 1213 | picocolors: 1.1.1 1214 | source-map-js: 1.2.1 1215 | 1216 | [email protected]: {} 1217 | 1218 | [email protected]: {} 1219 | 1220 | [email protected]: {} 1221 | 1222 | [email protected]: {} 1223 | 1224 | [email protected]: 1225 | dependencies: 1226 | '@types/estree': 1.0.6 1227 | optionalDependencies: 1228 | '@rollup/rollup-android-arm-eabi': 4.31.0 1229 | '@rollup/rollup-android-arm64': 4.31.0 1230 | '@rollup/rollup-darwin-arm64': 4.31.0 1231 | '@rollup/rollup-darwin-x64': 4.31.0 1232 | '@rollup/rollup-freebsd-arm64': 4.31.0 1233 | '@rollup/rollup-freebsd-x64': 4.31.0 1234 | '@rollup/rollup-linux-arm-gnueabihf': 4.31.0 1235 | '@rollup/rollup-linux-arm-musleabihf': 4.31.0 1236 | '@rollup/rollup-linux-arm64-gnu': 4.31.0 1237 | '@rollup/rollup-linux-arm64-musl': 4.31.0 1238 | '@rollup/rollup-linux-loongarch64-gnu': 4.31.0 1239 | '@rollup/rollup-linux-powerpc64le-gnu': 4.31.0 1240 | '@rollup/rollup-linux-riscv64-gnu': 4.31.0 1241 | '@rollup/rollup-linux-s390x-gnu': 4.31.0 1242 | '@rollup/rollup-linux-x64-gnu': 4.31.0 1243 | '@rollup/rollup-linux-x64-musl': 4.31.0 1244 | '@rollup/rollup-win32-arm64-msvc': 4.31.0 1245 | '@rollup/rollup-win32-ia32-msvc': 4.31.0 1246 | '@rollup/rollup-win32-x64-msvc': 4.31.0 1247 | fsevents: 2.3.3 1248 | 1249 | [email protected]: 1250 | dependencies: 1251 | shebang-regex: 3.0.0 1252 | 1253 | [email protected]: {} 1254 | 1255 | [email protected]: {} 1256 | 1257 | [email protected]: {} 1258 | 1259 | [email protected]: 1260 | dependencies: 1261 | has-flag: 4.0.0 1262 | 1263 | [email protected]: 1264 | dependencies: 1265 | prelude-ls: 1.2.1 1266 | 1267 | [email protected]: 1268 | optional: true 1269 | 1270 | [email protected]: 1271 | dependencies: 1272 | punycode: 2.3.1 1273 | 1274 | [email protected]: {} 1275 | 1276 | [email protected](@types/[email protected]): 1277 | dependencies: 1278 | esbuild: 0.21.5 1279 | postcss: 8.5.1 1280 | rollup: 4.31.0 1281 | optionalDependencies: 1282 | '@types/node': 22.10.1 1283 | fsevents: 2.3.3 1284 | 1285 | [email protected]: 1286 | dependencies: 1287 | isexe: 2.0.0 1288 | 1289 | [email protected]: {} 1290 | 1291 | [email protected]: {} 1292 |


/postcss.config.cjs:

1 | /* eslint-disable */ 2 | module.exports = { 3 | plugins: [require('postcss-nesting')], 4 | };


/src/arrow.js:

1 | import { createSVG } from './svg_utils'; 2 | 3 | export default class Arrow { 4 | constructor(gantt, from_task, to_task) { 5 | this.gantt = gantt; 6 | this.from_task = from_task; 7 | this.to_task = to_task; 8 | 9 | this.calculate_path(); 10 | this.draw(); 11 | } 12 | 13 | calculate_path() { 14 | let start_x = 15 | this.from_task.$bar.getX() + this.from_task.$bar.getWidth() / 2; 16 | 17 | const condition = () => 18 | this.to_task.$bar.getX() < start_x + this.gantt.options.padding && 19 | start_x > this.from_task.$bar.getX() + this.gantt.options.padding; 20 | 21 | while (condition()) { 22 | start_x -= 10; 23 | } 24 | start_x -= 10; 25 | 26 | let start_y = 27 | this.gantt.config.header_height + 28 | this.gantt.options.bar_height + 29 | (this.gantt.options.padding + this.gantt.options.bar_height) * 30 | this.from_task.task._index + 31 | this.gantt.options.padding / 2; 32 | 33 | let end_x = this.to_task.$bar.getX() - 13; 34 | let end_y = 35 | this.gantt.config.header_height + 36 | this.gantt.options.bar_height / 2 + 37 | (this.gantt.options.padding + this.gantt.options.bar_height) * 38 | this.to_task.task._index + 39 | this.gantt.options.padding / 2; 40 | 41 | const from_is_below_to = 42 | this.from_task.task._index > this.to_task.task._index; 43 | 44 | let curve = this.gantt.options.arrow_curve; 45 | const clockwise = from_is_below_to ? 1 : 0; 46 | let curve_y = from_is_below_to ? -curve : curve; 47 | 48 | if ( 49 | this.to_task.$bar.getX() <= 50 | this.from_task.$bar.getX() + this.gantt.options.padding 51 | ) { 52 | let down_1 = this.gantt.options.padding / 2 - curve; 53 | if (down_1 < 0) { 54 | down_1 = 0; 55 | curve = this.gantt.options.padding / 2; 56 | curve_y = from_is_below_to ? -curve : curve; 57 | } 58 | const down_2 = 59 | this.to_task.$bar.getY() + 60 | this.to_task.$bar.getHeight() / 2 - 61 | curve_y; 62 | const left = this.to_task.$bar.getX() - this.gantt.options.padding; 63 | this.path = 64 | M ${start_x} ${start_y} 65 | v ${down_1} 66 | a ${curve} ${curve} 0 0 1 ${-curve} ${curve} 67 | H ${left} 68 | a ${curve} ${curve} 0 0 ${clockwise} ${-curve} ${curve_y} 69 | V ${down_2} 70 | a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve_y} 71 | L ${end_x} ${end_y} 72 | m -5 -5 73 | l 5 5 74 | l -5 5; 75 | } else { 76 | if (end_x < start_x + curve) curve = end_x - start_x; 77 | 78 | let offset = from_is_below_to ? end_y + curve : end_y - curve; 79 | 80 | this.path = 81 | M ${start_x} ${start_y} 82 | V ${offset} 83 | a ${curve} ${curve} 0 0 ${clockwise} ${curve} ${curve} 84 | L ${end_x} ${end_y} 85 | m -5 -5 86 | l 5 5 87 | l -5 5; 88 | } 89 | } 90 | 91 | draw() { 92 | this.element = createSVG('path', { 93 | d: this.path, 94 | 'data-from': this.from_task.task.id, 95 | 'data-to': this.to_task.task.id, 96 | }); 97 | } 98 | 99 | update() { 100 | this.calculate_path(); 101 | this.element.setAttribute('d', this.path); 102 | } 103 | } 104 |


/src/bar.js:

1 | import date_utils from './date_utils'; 2 | import { $, createSVG, animateSVG } from './svg_utils'; 3 | 4 | export default class Bar { 5 | constructor(gantt, task) { 6 | this.set_defaults(gantt, task); 7 | this.prepare_wrappers(); 8 | this.prepare_helpers(); 9 | this.refresh(); 10 | } 11 | 12 | refresh() { 13 | this.bar_group.innerHTML = ''; 14 | this.handle_group.innerHTML = ''; 15 | if (this.task.custom_class) { 16 | this.group.classList.add(this.task.custom_class); 17 | } else { 18 | this.group.classList = ['bar-wrapper']; 19 | } 20 | 21 | this.prepare_values(); 22 | this.draw(); 23 | this.bind(); 24 | } 25 | 26 | set_defaults(gantt, task) { 27 | this.action_completed = false; 28 | this.gantt = gantt; 29 | this.task = task; 30 | this.name = this.name || ''; 31 | } 32 | 33 | prepare_wrappers() { 34 | this.group = createSVG('g', { 35 | class: 36 | 'bar-wrapper' + 37 | (this.task.custom_class ? ' ' + this.task.custom_class : ''), 38 | 'data-id': this.task.id, 39 | }); 40 | this.bar_group = createSVG('g', { 41 | class: 'bar-group', 42 | append_to: this.group, 43 | }); 44 | this.handle_group = createSVG('g', { 45 | class: 'handle-group', 46 | append_to: this.group, 47 | }); 48 | } 49 | 50 | prepare_values() { 51 | this.invalid = this.task.invalid; 52 | this.height = this.gantt.options.bar_height; 53 | this.image_size = this.height - 5; 54 | this.task.start = new Date(this.task.start); 55 | this.task.end = new Date(this.task.end); 56 | this.compute_x(); 57 | this.compute_y(); 58 | this.compute_duration(); 59 | this.corner_radius = this.gantt.options.bar_corner_radius; 60 | this.width = this.gantt.config.column_width * this.duration; 61 | if (!this.task.progress || this.task.progress < 0) 62 | this.task.progress = 0; 63 | if (this.task.progress > 100) this.task.progress = 100; 64 | } 65 | 66 | prepare_helpers() { 67 | SVGElement.prototype.getX = function () { 68 | return +this.getAttribute('x'); 69 | }; 70 | SVGElement.prototype.getY = function () { 71 | return +this.getAttribute('y'); 72 | }; 73 | SVGElement.prototype.getWidth = function () { 74 | return +this.getAttribute('width'); 75 | }; 76 | SVGElement.prototype.getHeight = function () { 77 | return +this.getAttribute('height'); 78 | }; 79 | SVGElement.prototype.getEndX = function () { 80 | return this.getX() + this.getWidth(); 81 | }; 82 | } 83 | 84 | prepare_expected_progress_values() { 85 | this.compute_expected_progress(); 86 | this.expected_progress_width = 87 | this.gantt.options.column_width * 88 | this.duration * 89 | (this.expected_progress / 100) || 0; 90 | } 91 | 92 | draw() { 93 | this.draw_bar(); 94 | this.draw_progress_bar(); 95 | if (this.gantt.options.show_expected_progress) { 96 | this.prepare_expected_progress_values(); 97 | this.draw_expected_progress_bar(); 98 | } 99 | this.draw_label(); 100 | this.draw_resize_handles(); 101 | 102 | if (this.task.thumbnail) { 103 | this.draw_thumbnail(); 104 | } 105 | } 106 | 107 | draw_bar() { 108 | this.$bar = createSVG('rect', { 109 | x: this.x, 110 | y: this.y, 111 | width: this.width, 112 | height: this.height, 113 | rx: this.corner_radius, 114 | ry: this.corner_radius, 115 | class: 'bar', 116 | append_to: this.bar_group, 117 | }); 118 | if (this.task.color) this.$bar.style.fill = this.task.color; 119 | animateSVG(this.$bar, 'width', 0, this.width); 120 | 121 | if (this.invalid) { 122 | this.$bar.classList.add('bar-invalid'); 123 | } 124 | } 125 | 126 | draw_expected_progress_bar() { 127 | if (this.invalid) return; 128 | this.$expected_bar_progress = createSVG('rect', { 129 | x: this.x, 130 | y: this.y, 131 | width: this.expected_progress_width, 132 | height: this.height, 133 | rx: this.corner_radius, 134 | ry: this.corner_radius, 135 | class: 'bar-expected-progress', 136 | append_to: this.bar_group, 137 | }); 138 | 139 | animateSVG( 140 | this.$expected_bar_progress, 141 | 'width', 142 | 0, 143 | this.expected_progress_width, 144 | ); 145 | } 146 | 147 | draw_progress_bar() { 148 | if (this.invalid) return; 149 | this.progress_width = this.calculate_progress_width(); 150 | let r = this.corner_radius; 151 | if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) 152 | r = this.corner_radius + 2; 153 | this.$bar_progress = createSVG('rect', { 154 | x: this.x, 155 | y: this.y, 156 | width: this.progress_width, 157 | height: this.height, 158 | rx: r, 159 | ry: r, 160 | class: 'bar-progress', 161 | append_to: this.bar_group, 162 | }); 163 | if (this.task.color_progress) 164 | this.$bar_progress.style.fill = this.task.color_progress; 165 | const x = 166 | (date_utils.diff( 167 | this.task.start, 168 | this.gantt.gantt_start, 169 | this.gantt.config.unit, 170 | ) / 171 | this.gantt.config.step) * 172 | this.gantt.config.column_width; 173 | 174 | let $date_highlight = this.gantt.create_el({ 175 | classes: date-range-highlight hide highlight-${this.task.id}, 176 | width: this.width, 177 | left: x, 178 | }); 179 | this.$date_highlight = $date_highlight; 180 | this.gantt.$lower_header.prepend(this.$date_highlight); 181 | 182 | animateSVG(this.$bar_progress, 'width', 0, this.progress_width); 183 | } 184 | 185 | calculate_progress_width() { 186 | const width = this.$bar.getWidth(); 187 | const ignored_end = this.x + width; 188 | const total_ignored_area = 189 | this.gantt.config.ignored_positions.reduce((acc, val) => { 190 | return acc + (val >= this.x && val < ignored_end); 191 | }, 0) * this.gantt.config.column_width; 192 | let progress_width = 193 | ((width - total_ignored_area) * this.task.progress) / 100; 194 | const progress_end = this.x + progress_width; 195 | const total_ignored_progress = 196 | this.gantt.config.ignored_positions.reduce((acc, val) => { 197 | return acc + (val >= this.x && val < progress_end); 198 | }, 0) * this.gantt.config.column_width; 199 | 200 | progress_width += total_ignored_progress; 201 | 202 | let ignored_regions = this.gantt.get_ignored_region( 203 | this.x + progress_width, 204 | ); 205 | 206 | while (ignored_regions.length) { 207 | progress_width += this.gantt.config.column_width; 208 | ignored_regions = this.gantt.get_ignored_region( 209 | this.x + progress_width, 210 | ); 211 | } 212 | this.progress_width = progress_width; 213 | return progress_width; 214 | } 215 | 216 | draw_label() { 217 | let x_coord = this.x + this.$bar.getWidth() / 2; 218 | 219 | if (this.task.thumbnail) { 220 | x_coord = this.x + this.image_size + 5; 221 | } 222 | 223 | createSVG('text', { 224 | x: x_coord, 225 | y: this.y + this.height / 2, 226 | innerHTML: this.task.name, 227 | class: 'bar-label', 228 | append_to: this.bar_group, 229 | }); 230 | // labels get BBox in the next tick 231 | requestAnimationFrame(() => this.update_label_position()); 232 | } 233 | 234 | draw_thumbnail() { 235 | let x_offset = 10, 236 | y_offset = 2; 237 | let defs, clipPath; 238 | 239 | defs = createSVG('defs', { 240 | append_to: this.bar_group, 241 | }); 242 | 243 | createSVG('rect', { 244 | id: 'rect' + this.task.id, 245 | x: this.x + x_offset, 246 | y: this.y + y_offset, 247 | width: this.image_size, 248 | height: this.image_size, 249 | rx: '15', 250 | class: 'img_mask', 251 | append_to: defs, 252 | }); 253 | 254 | clipPath = createSVG('clipPath', { 255 | id: 'clip' + this.task.id, 256 | append_to: defs, 257 | }); 258 | 259 | createSVG('use', { 260 | href: '#rect' + this.task.id, 261 | append_to: clipPath, 262 | }); 263 | 264 | createSVG('image', { 265 | x: this.x + x_offset, 266 | y: this.y + y_offset, 267 | width: this.image_size, 268 | height: this.image_size, 269 | class: 'bar-img', 270 | href: this.task.thumbnail, 271 | clipPath: 'clip_' + this.task.id, 272 | append_to: this.bar_group, 273 | }); 274 | } 275 | 276 | draw_resize_handles() { 277 | if (this.invalid || this.gantt.options.readonly) return; 278 | 279 | const bar = this.$bar; 280 | const handle_width = 3; 281 | this.handles = []; 282 | if (!this.gantt.options.readonly_dates) { 283 | this.handles.push( 284 | createSVG('rect', { 285 | x: bar.getEndX() - handle_width / 2, 286 | y: bar.getY() + this.height / 4, 287 | width: handle_width, 288 | height: this.height / 2, 289 | rx: 2, 290 | ry: 2, 291 | class: 'handle right', 292 | append_to: this.handle_group, 293 | }), 294 | ); 295 | 296 | this.handles.push( 297 | createSVG('rect', { 298 | x: bar.getX() - handle_width / 2, 299 | y: bar.getY() + this.height / 4, 300 | width: handle_width, 301 | height: this.height / 2, 302 | rx: 2, 303 | ry: 2, 304 | class: 'handle left', 305 | append_to: this.handle_group, 306 | }), 307 | ); 308 | } 309 | if (!this.gantt.options.readonly_progress) { 310 | const bar_progress = this.$bar_progress; 311 | this.$handle_progress = createSVG('circle', { 312 | cx: bar_progress.getEndX(), 313 | cy: bar_progress.getY() + bar_progress.getHeight() / 2, 314 | r: 4.5, 315 | class: 'handle progress', 316 | append_to: this.handle_group, 317 | }); 318 | this.handles.push(this.$handle_progress); 319 | } 320 | 321 | for (let handle of this.handles) { 322 | $.on(handle, 'mouseenter', () =&gt; handle.classList.add('active')); 323 | $.on(handle, 'mouseleave', () => handle.classList.remove('active')); 324 | } 325 | } 326 | 327 | bind() { 328 | if (this.invalid) return; 329 | this.setup_click_event(); 330 | } 331 | 332 | setup_click_event() { 333 | let task_id = this.task.id; 334 | $.on(this.group, 'mouseover', (e) =&gt; { 335 | this.gantt.trigger_event('hover', [ 336 | this.task, 337 | e.screenX, 338 | e.screenY, 339 | e, 340 | ]); 341 | }); 342 | 343 | if (this.gantt.options.popup_on === 'click') { 344 | $.on(this.group, 'mouseup', (e) => { 345 | const posX = e.offsetX || e.layerX; 346 | if (this.$handle_progress) { 347 | const cx = +this.$handle_progress.getAttribute('cx'); 348 | if (cx > posX - 1 && cx < posX + 1) return; 349 | if (this.gantt.bar_being_dragged) return; 350 | } 351 | this.gantt.show_popup({ 352 | x: e.offsetX || e.layerX, 353 | y: e.offsetY || e.layerY, 354 | task: this.task, 355 | target: this.$bar, 356 | }); 357 | }); 358 | } 359 | let timeout; 360 | $.on(this.group, 'mouseenter', (e) => { 361 | timeout = setTimeout(() => { 362 | if (this.gantt.options.popup_on === 'hover') 363 | this.gantt.show_popup({ 364 | x: e.offsetX || e.layerX, 365 | y: e.offsetY || e.layerY, 366 | task: this.task, 367 | target: this.$bar, 368 | }); 369 | this.gantt.$container 370 | .querySelector(.highlight-${task_id}) 371 | .classList.remove('hide'); 372 | }, 200); 373 | }); 374 | $.on(this.group, 'mouseleave', () => { 375 | clearTimeout(timeout); 376 | if (this.gantt.options.popup_on === 'hover') 377 | this.gantt.popup?.hide?.(); 378 | this.gantt.$container 379 | .querySelector(.highlight-${task_id}) 380 | .classList.add('hide'); 381 | }); 382 | 383 | $.on(this.group, 'click', () =&gt; { 384 | this.gantt.trigger_event('click', [this.task]); 385 | }); 386 | 387 | $.on(this.group, 'dblclick', (e) => { 388 | if (this.action_completed) { 389 | // just finished a move action, wait for a few seconds 390 | return; 391 | } 392 | this.group.classList.remove('active'); 393 | if (this.gantt.popup) 394 | this.gantt.popup.parent.classList.remove('hide'); 395 | 396 | this.gantt.trigger_event('double_click', [this.task]); 397 | }); 398 | let tapedTwice = false; 399 | $.on(this.group, 'touchstart', (e) => { 400 | if (!tapedTwice) { 401 | tapedTwice = true; 402 | setTimeout(function () { tapedTwice = false; }, 300); 403 | return false; 404 | } 405 | e.preventDefault(); 406 | //action on double tap goes below 407 | 408 | 409 | if (this.action_completed) { 410 | // just finished a move action, wait for a few seconds 411 | return; 412 | } 413 | this.group.classList.remove('active'); 414 | if (this.gantt.popup) 415 | this.gantt.popup.parent.classList.remove('hide'); 416 | 417 | this.gantt.trigger_event('double_click', [this.task]); 418 | }); 419 | } 420 | 421 | update_bar_position({ x = null, width = null }) { 422 | const bar = this.$bar; 423 | 424 | if (x) { 425 | const xs = this.task.dependencies.map((dep) => { 426 | return this.gantt.get_bar(dep).$bar.getX(); 427 | }); 428 | const valid_x = xs.reduce((prev, curr) => { 429 | return prev && x >= curr; 430 | }, true); 431 | if (!valid_x) return; 432 | this.update_attr(bar, 'x', x); 433 | this.x = x; 434 | this.$date_highlight.style.left = x + 'px'; 435 | } 436 | if (width > 0) { 437 | this.update_attr(bar, 'width', width); 438 | this.$date_highlight.style.width = width + 'px'; 439 | } 440 | 441 | this.update_label_position(); 442 | this.update_handle_position(); 443 | this.date_changed(); 444 | this.compute_duration(); 445 | 446 | if (this.gantt.options.show_expected_progress) { 447 | this.update_expected_progressbar_position(); 448 | } 449 | 450 | this.update_progressbar_position(); 451 | this.update_arrow_position(); 452 | } 453 | 454 | update_label_position_on_horizontal_scroll({ x, sx }) { 455 | const container = 456 | this.gantt.$container.querySelector('.gantt-container'); 457 | const label = this.group.querySelector('.bar-label'); 458 | const img = this.group.querySelector('.bar-img') || ''; 459 | const img_mask = this.bar_group.querySelector('.img_mask') || ''; 460 | 461 | let barWidthLimit = this.$bar.getX() + this.$bar.getWidth(); 462 | let newLabelX = label.getX() + x; 463 | let newImgX = (img && img.getX() + x) || 0; 464 | let imgWidth = (img && img.getBBox().width + 7) || 7; 465 | let labelEndX = newLabelX + label.getBBox().width + 7; 466 | let viewportCentral = sx + container.clientWidth / 2; 467 | 468 | if (label.classList.contains('big')) return; 469 | 470 | if (labelEndX < barWidthLimit && x > 0 && labelEndX < viewportCentral) { 471 | label.setAttribute('x', newLabelX); 472 | if (img) { 473 | img.setAttribute('x', newImgX); 474 | img_mask.setAttribute('x', newImgX); 475 | } 476 | } else if ( 477 | newLabelX - imgWidth > this.$bar.getX() && 478 | x < 0 && 479 | labelEndX > viewportCentral 480 | ) { 481 | label.setAttribute('x', newLabelX); 482 | if (img) { 483 | img.setAttribute('x', newImgX); 484 | img_mask.setAttribute('x', newImgX); 485 | } 486 | } 487 | } 488 | 489 | date_changed() { 490 | let changed = false; 491 | const { new_start_date, new_end_date } = this.compute_start_end_date(); 492 | if (Number(this.task._start) !== Number(new_start_date)) { 493 | changed = true; 494 | this.task._start = new_start_date; 495 | } 496 | 497 | if (Number(this.task._end) !== Number(new_end_date)) { 498 | changed = true; 499 | this.task._end = new_end_date; 500 | } 501 | 502 | if (!changed) return; 503 | 504 | this.gantt.trigger_event('date_change', [ 505 | this.task, 506 | new_start_date, 507 | date_utils.add(new_end_date, -1, 'second'), 508 | ]); 509 | } 510 | 511 | progress_changed() { 512 | this.task.progress = this.compute_progress(); 513 | this.gantt.trigger_event('progress_change', [ 514 | this.task, 515 | this.task.progress, 516 | ]); 517 | } 518 | 519 | set_action_completed() { 520 | this.action_completed = true; 521 | setTimeout(() => (this.action_completed = false), 1000); 522 | } 523 | 524 | compute_start_end_date() { 525 | const bar = this.$bar; 526 | const x_in_units = bar.getX() / this.gantt.config.column_width; 527 | let new_start_date = date_utils.add( 528 | this.gantt.gantt_start, 529 | x_in_units * this.gantt.config.step, 530 | this.gantt.config.unit, 531 | ); 532 | 533 | const width_in_units = bar.getWidth() / this.gantt.config.column_width; 534 | const new_end_date = date_utils.add( 535 | new_start_date, 536 | width_in_units * this.gantt.config.step, 537 | this.gantt.config.unit, 538 | ); 539 | 540 | return { new_start_date, new_end_date }; 541 | } 542 | 543 | compute_progress() { 544 | this.progress_width = this.$bar_progress.getWidth(); 545 | this.x = this.$bar_progress.getBBox().x; 546 | const progress_area = this.x + this.progress_width; 547 | const progress = 548 | this.progress_width - 549 | this.gantt.config.ignored_positions.reduce((acc, val) => { 550 | return acc + (val >= this.x && val <= progress_area); 551 | }, 0) * 552 | this.gantt.config.column_width; 553 | if (progress < 0) return 0; 554 | const total = 555 | this.$bar.getWidth() - 556 | this.ignored_duration_raw * this.gantt.config.column_width; 557 | return parseInt((progress / total) * 100, 10); 558 | } 559 | 560 | compute_expected_progress() { 561 | this.expected_progress = 562 | date_utils.diff(date_utils.today(), this.task._start, 'hour') / 563 | this.gantt.config.step; 564 | this.expected_progress = 565 | ((this.expected_progress < this.duration 566 | ? this.expected_progress 567 | : this.duration) * 568 | 100) / 569 | this.duration; 570 | } 571 | 572 | compute_x() { 573 | const { column_width } = this.gantt.config; 574 | const task_start = this.task._start; 575 | const gantt_start = this.gantt.gantt_start; 576 | 577 | const diff = 578 | date_utils.diff(task_start, gantt_start, this.gantt.config.unit) / 579 | this.gantt.config.step; 580 | 581 | let x = diff * column_width; 582 | 583 | /* Since the column width is based on 30, 584 | we count the month-difference, multiply it by 30 for a "pseudo-month" 585 | and then add the days in the month, making sure the number does not exceed 29 586 | so it is within the column */ 587 | 588 | // if (this.gantt.view_is('Month')) { 589 | // const diffDaysBasedOn30DayMonths = 590 | // date_utils.diff(task_start, gantt_start, 'month') * 30; 591 | // const dayInMonth = Math.min( 592 | // 29, 593 | // date_utils.format( 594 | // task_start, 595 | // 'DD', 596 | // this.gantt.options.language, 597 | // ), 598 | // ); 599 | // const diff = diffDaysBasedOn30DayMonths + dayInMonth; 600 | 601 | // x = (diff * column_width) / 30; 602 | // } 603 | 604 | this.x = x; 605 | } 606 | 607 | compute_y() { 608 | this.y = 609 | this.gantt.config.header_height + 610 | this.gantt.options.padding / 2 + 611 | this.task._index * (this.height + this.gantt.options.padding); 612 | } 613 | 614 | compute_duration() { 615 | let actual_duration_in_days = 0, 616 | duration_in_days = 0; 617 | for ( 618 | let d = new Date(this.task._start); 619 | d < this.task._end; 620 | d.setDate(d.getDate() + 1) 621 | ) { 622 | duration_in_days++; 623 | if ( 624 | !this.gantt.config.ignored_dates.find( 625 | (k) => k.getTime() === d.getTime(), 626 | ) && 627 | (!this.gantt.config.ignored_function || 628 | !this.gantt.config.ignored_function(d)) 629 | ) { 630 | actual_duration_in_days++; 631 | } 632 | } 633 | this.task.actual_duration = actual_duration_in_days; 634 | this.task.ignored_duration = duration_in_days - actual_duration_in_days; 635 | 636 | this.duration = 637 | date_utils.convert_scales( 638 | duration_in_days + 'd', 639 | this.gantt.config.unit, 640 | ) / this.gantt.config.step; 641 | 642 | this.actual_duration_raw = 643 | date_utils.convert_scales( 644 | actual_duration_in_days + 'd', 645 | this.gantt.config.unit, 646 | ) / this.gantt.config.step; 647 | 648 | this.ignored_duration_raw = this.duration - this.actual_duration_raw; 649 | } 650 | 651 | update_attr(element, attr, value) { 652 | value = +value; 653 | if (!isNaN(value)) { 654 | element.setAttribute(attr, value); 655 | } 656 | return element; 657 | } 658 | 659 | update_expected_progressbar_position() { 660 | if (this.invalid) return; 661 | this.$expected_bar_progress.setAttribute('x', this.$bar.getX()); 662 | this.compute_expected_progress(); 663 | this.$expected_bar_progress.setAttribute( 664 | 'width', 665 | this.gantt.config.column_width * 666 | this.actual_duration_raw * 667 | (this.expected_progress / 100) || 0, 668 | ); 669 | } 670 | 671 | update_progressbar_position() { 672 | if (this.invalid || this.gantt.options.readonly) return; 673 | this.$bar_progress.setAttribute('x', this.$bar.getX()); 674 | 675 | this.$bar_progress.setAttribute( 676 | 'width', 677 | this.calculate_progress_width(), 678 | ); 679 | } 680 | 681 | update_label_position() { 682 | const img_mask = this.bar_group.querySelector('.img_mask') || ''; 683 | const bar = this.$bar, 684 | label = this.group.querySelector('.bar-label'), 685 | img = this.group.querySelector('.bar-img'); 686 | 687 | let padding = 5; 688 | let x_offset_label_img = this.image_size + 10; 689 | const labelWidth = label.getBBox().width; 690 | const barWidth = bar.getWidth(); 691 | if (labelWidth > barWidth) { 692 | label.classList.add('big'); 693 | if (img) { 694 | img.setAttribute('x', bar.getEndX() + padding); 695 | img_mask.setAttribute('x', bar.getEndX() + padding); 696 | label.setAttribute('x', bar.getEndX() + x_offset_label_img); 697 | } else { 698 | label.setAttribute('x', bar.getEndX() + padding); 699 | } 700 | } else { 701 | label.classList.remove('big'); 702 | if (img) { 703 | img.setAttribute('x', bar.getX() + padding); 704 | img_mask.setAttribute('x', bar.getX() + padding); 705 | label.setAttribute( 706 | 'x', 707 | bar.getX() + barWidth / 2 + x_offset_label_img, 708 | ); 709 | } else { 710 | label.setAttribute( 711 | 'x', 712 | bar.getX() + barWidth / 2 - labelWidth / 2, 713 | ); 714 | } 715 | } 716 | } 717 | 718 | update_handle_position() { 719 | if (this.invalid || this.gantt.options.readonly) return; 720 | const bar = this.$bar; 721 | this.handle_group 722 | .querySelector('.handle.left') 723 | .setAttribute('x', bar.getX()); 724 | this.handle_group 725 | .querySelector('.handle.right') 726 | .setAttribute('x', bar.getEndX()); 727 | const handle = this.group.querySelector('.handle.progress'); 728 | handle && handle.setAttribute('cx', this.$bar_progress.getEndX()); 729 | } 730 | 731 | update_arrow_position() { 732 | this.arrows = this.arrows || []; 733 | for (let arrow of this.arrows) { 734 | arrow.update(); 735 | } 736 | } 737 | } 738 |


/src/date_utils.js:

1 | const YEAR = 'year'; 2 | const MONTH = 'month'; 3 | const DAY = 'day'; 4 | const HOUR = 'hour'; 5 | const MINUTE = 'minute'; 6 | const SECOND = 'second'; 7 | const MILLISECOND = 'millisecond'; 8 | 9 | export default { 10 | parse_duration(duration) { 11 | const regex = /([0-9]+)(y|m|d|h|min|s|ms)/gm; 12 | const matches = regex.exec(duration); 13 | if (matches !== null) { 14 | if (matches[2] === 'y') { 15 | return { duration: parseInt(matches[1]), scale: year }; 16 | } else if (matches[2] === 'm') { 17 | return { duration: parseInt(matches[1]), scale: month }; 18 | } else if (matches[2] === 'd') { 19 | return { duration: parseInt(matches[1]), scale: day }; 20 | } else if (matches[2] === 'h') { 21 | return { duration: parseInt(matches[1]), scale: hour }; 22 | } else if (matches[2] === 'min') { 23 | return { duration: parseInt(matches[1]), scale: minute }; 24 | } else if (matches[2] === 's') { 25 | return { duration: parseInt(matches[1]), scale: second }; 26 | } else if (matches[2] === 'ms') { 27 | return { duration: parseInt(matches[1]), scale: millisecond }; 28 | } 29 | } 30 | }, 31 | parse(date, date_separator = '-', time_separator = /[.:]/) { 32 | if (date instanceof Date) { 33 | return date; 34 | } 35 | if (typeof date === 'string') { 36 | let date_parts, time_parts; 37 | const parts = date.split(' '); 38 | date_parts = parts[0] 39 | .split(date_separator) 40 | .map((val) => parseInt(val, 10)); 41 | time_parts = parts[1] && parts[1].split(time_separator); 42 | 43 | // month is 0 indexed 44 | date_parts[1] = date_parts[1] ? date_parts[1] - 1 : 0; 45 | 46 | let vals = date_parts; 47 | 48 | if (time_parts && time_parts.length) { 49 | if (time_parts.length === 4) { 50 | time_parts[3] = '0.' + time_parts[3]; 51 | time_parts[3] = parseFloat(time_parts[3]) * 1000; 52 | } 53 | vals = vals.concat(time_parts); 54 | } 55 | return new Date(...vals); 56 | } 57 | }, 58 | 59 | to_string(date, with_time = false) { 60 | if (!(date instanceof Date)) { 61 | throw new TypeError('Invalid argument type'); 62 | } 63 | const vals = this.get_date_values(date).map((val, i) => { 64 | if (i === 1) { 65 | // add 1 for month 66 | val = val + 1; 67 | } 68 | 69 | if (i === 6) { 70 | return padStart(val + '', 3, '0'); 71 | } 72 | 73 | return padStart(val + '', 2, '0'); 74 | }); 75 | const date_string = ${vals[0]}-${vals[1]}-${vals[2]}; 76 | const time_string = ${vals[3]}:${vals[4]}:${vals[5]}.${vals[6]}; 77 | 78 | return date_string + (with_time ? ' ' + time_string : ''); 79 | }, 80 | 81 | format(date, date_format = 'YYYY-MM-DD HH:mm:ss.SSS', lang = 'en') { 82 | const dateTimeFormat = new Intl.DateTimeFormat(lang, { 83 | month: 'long', 84 | }); 85 | const dateTimeFormatShort = new Intl.DateTimeFormat(lang, { 86 | month: 'short', 87 | }); 88 | const month_name = dateTimeFormat.format(date); 89 | const month_name_capitalized = 90 | month_name.charAt(0).toUpperCase() + month_name.slice(1); 91 | 92 | const values = this.get_date_values(date).map((d) => padStart(d, 2, 0)); 93 | const format_map = { 94 | YYYY: values[0], 95 | MM: padStart(+values[1] + 1, 2, 0), 96 | DD: values[2], 97 | HH: values[3], 98 | mm: values[4], 99 | ss: values[5], 100 | SSS: values[6], 101 | D: values[2], 102 | MMMM: month_name_capitalized, 103 | MMM: dateTimeFormatShort.format(date), 104 | }; 105 | 106 | let str = date_format; 107 | const formatted_values = []; 108 | 109 | Object.keys(format_map) 110 | .sort((a, b) => b.length - a.length) // big string first 111 | .forEach((key) => { 112 | if (str.includes(key)) { 113 | str = str.replaceAll(key, ${formatted_values.length}); 114 | formatted_values.push(format_map[key]); 115 | } 116 | }); 117 | 118 | formatted_values.forEach((value, i) => { 119 | str = str.replaceAll(${i}, value); 120 | }); 121 | 122 | return str; 123 | }, 124 | 125 | diff(date_a, date_b, scale = 'day') { 126 | let milliseconds, seconds, hours, minutes, days, months, years; 127 | 128 | milliseconds = 129 | date_a - 130 | date_b + 131 | (date_b.getTimezoneOffset() - date_a.getTimezoneOffset()) * 60000; 132 | seconds = milliseconds / 1000; 133 | minutes = seconds / 60; 134 | hours = minutes / 60; 135 | days = hours / 24; 136 | // Calculate months across years 137 | let yearDiff = date_a.getFullYear() - date_b.getFullYear(); 138 | let monthDiff = date_a.getMonth() - date_b.getMonth(); 139 | // calculate extra 140 | monthDiff += (days % 30) / 30; 141 | 142 | /* If monthDiff is negative, date_b is in an earlier month than 143 | date_a and thus subtracted from the year difference in months / 144 | months = yearDiff * 12 + monthDiff; 145 | / If date_a's (e.g. march 1st) day of the month is smaller than date_b (e.g. february 28th), 146 | adjust the month difference */ 147 | if (date_a.getDate() < date_b.getDate()) { 148 | months--; 149 | } 150 | 151 | // Calculate years based on actual months 152 | years = months / 12; 153 | 154 | if (!scale.endsWith('s')) { 155 | scale += 's'; 156 | } 157 | 158 | return ( 159 | Math.round( 160 | { 161 | milliseconds, 162 | seconds, 163 | minutes, 164 | hours, 165 | days, 166 | months, 167 | years, 168 | }[scale] * 100, 169 | ) / 100 170 | ); 171 | }, 172 | 173 | today() { 174 | const vals = this.get_date_values(new Date()).slice(0, 3); 175 | return new Date(...vals); 176 | }, 177 | 178 | now() { 179 | return new Date(); 180 | }, 181 | 182 | add(date, qty, scale) { 183 | qty = parseInt(qty, 10); 184 | const vals = [ 185 | date.getFullYear() + (scale === YEAR ? qty : 0), 186 | date.getMonth() + (scale === MONTH ? qty : 0), 187 | date.getDate() + (scale === DAY ? qty : 0), 188 | date.getHours() + (scale === HOUR ? qty : 0), 189 | date.getMinutes() + (scale === MINUTE ? qty : 0), 190 | date.getSeconds() + (scale === SECOND ? qty : 0), 191 | date.getMilliseconds() + (scale === MILLISECOND ? qty : 0), 192 | ]; 193 | return new Date(...vals); 194 | }, 195 | 196 | start_of(date, scale) { 197 | const scores = { 198 | [YEAR]: 6, 199 | [MONTH]: 5, 200 | [DAY]: 4, 201 | [HOUR]: 3, 202 | [MINUTE]: 2, 203 | [SECOND]: 1, 204 | [MILLISECOND]: 0, 205 | }; 206 | 207 | function should_reset(_scale) { 208 | const max_score = scores[scale]; 209 | return scores[_scale] <= max_score; 210 | } 211 | 212 | const vals = [ 213 | date.getFullYear(), 214 | should_reset(YEAR) ? 0 : date.getMonth(), 215 | should_reset(MONTH) ? 1 : date.getDate(), 216 | should_reset(DAY) ? 0 : date.getHours(), 217 | should_reset(HOUR) ? 0 : date.getMinutes(), 218 | should_reset(MINUTE) ? 0 : date.getSeconds(), 219 | should_reset(SECOND) ? 0 : date.getMilliseconds(), 220 | ]; 221 | 222 | return new Date(...vals); 223 | }, 224 | 225 | clone(date) { 226 | return new Date(...this.get_date_values(date)); 227 | }, 228 | 229 | get_date_values(date) { 230 | return [ 231 | date.getFullYear(), 232 | date.getMonth(), 233 | date.getDate(), 234 | date.getHours(), 235 | date.getMinutes(), 236 | date.getSeconds(), 237 | date.getMilliseconds(), 238 | ]; 239 | }, 240 | 241 | convert_scales(period, to_scale) { 242 | const TO_DAYS = { 243 | millisecond: 1 / 60 / 60 / 24 / 1000, 244 | second: 1 / 60 / 60 / 24, 245 | minute: 1 / 60 / 24, 246 | hour: 1 / 24, 247 | day: 1, 248 | month: 30, 249 | year: 365, 250 | }; 251 | const { duration, scale } = this.parse_duration(period); 252 | let in_days = duration * TO_DAYS[scale]; 253 | return in_days / TO_DAYS[to_scale]; 254 | }, 255 | 256 | get_days_in_month(date) { 257 | const no_of_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 258 | 259 | const month = date.getMonth(); 260 | 261 | if (month !== 1) { 262 | return no_of_days[month]; 263 | } 264 | 265 | // Feb 266 | const year = date.getFullYear(); 267 | if ((year % 4 === 0 && year % 100 != 0) || year % 400 === 0) { 268 | return 29; 269 | } 270 | return 28; 271 | }, 272 | 273 | get_days_in_year(date) { 274 | return date.getFullYear() % 4 ? 365 : 366; 275 | }, 276 | }; 277 | 278 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart 279 | function padStart(str, targetLength, padString) { 280 | str = str + ''; 281 | targetLength = targetLength >> 0; 282 | padString = String(typeof padString !== 'undefined' ? padString : ' '); 283 | if (str.length > targetLength) { 284 | return String(str); 285 | } else { 286 | targetLength = targetLength - str.length; 287 | if (targetLength > padString.length) { 288 | padString += padString.repeat(targetLength / padString.length); 289 | } 290 | return padString.slice(0, targetLength) + String(str); 291 | } 292 | } 293 |


/src/defaults.js:

1 | import date_utils from './date_utils'; 2 | 3 | function getDecade(d) { 4 | const year = d.getFullYear(); 5 | return year - (year % 10) + ''; 6 | } 7 | 8 | function formatWeek(d, ld, lang) { 9 | let endOfWeek = date_utils.add(d, 6, 'day'); 10 | let endFormat = endOfWeek.getMonth() !== d.getMonth() ? 'D MMM' : 'D'; 11 | let beginFormat = !ld || d.getMonth() !== ld.getMonth() ? 'D MMM' : 'D'; 12 | return ${date_utils.format(d, beginFormat, lang)} - ${date_utils.format(endOfWeek, endFormat, lang)}; 13 | } 14 | 15 | const DEFAULT_VIEW_MODES = [ 16 | { 17 | name: 'Hour', 18 | padding: '7d', 19 | step: '1h', 20 | date_format: 'YYYY-MM-DD HH:', 21 | lower_text: 'HH', 22 | upper_text: (d, ld, lang) => 23 | !ld || d.getDate() !== ld.getDate() 24 | ? date_utils.format(d, 'D MMMM', lang) 25 | : '', 26 | upper_text_frequency: 24, 27 | }, 28 | { 29 | name: 'Quarter Day', 30 | padding: '7d', 31 | step: '6h', 32 | date_format: 'YYYY-MM-DD HH:', 33 | lower_text: 'HH', 34 | upper_text: (d, ld, lang) => 35 | !ld || d.getDate() !== ld.getDate() 36 | ? date_utils.format(d, 'D MMM', lang) 37 | : '', 38 | upper_text_frequency: 4, 39 | }, 40 | { 41 | name: 'Half Day', 42 | padding: '14d', 43 | step: '12h', 44 | date_format: 'YYYY-MM-DD HH:', 45 | lower_text: 'HH', 46 | upper_text: (d, ld, lang) => 47 | !ld || d.getDate() !== ld.getDate() 48 | ? d.getMonth() !== d.getMonth() 49 | ? date_utils.format(d, 'D MMM', lang) 50 | : date_utils.format(d, 'D', lang) 51 | : '', 52 | upper_text_frequency: 2, 53 | }, 54 | { 55 | name: 'Day', 56 | padding: '7d', 57 | date_format: 'YYYY-MM-DD', 58 | step: '1d', 59 | lower_text: (d, ld, lang) => 60 | !ld || d.getDate() !== ld.getDate() 61 | ? date_utils.format(d, 'D', lang) 62 | : '', 63 | upper_text: (d, ld, lang) => 64 | !ld || d.getMonth() !== ld.getMonth() 65 | ? date_utils.format(d, 'MMMM', lang) 66 | : '', 67 | thick_line: (d) => d.getDay() === 1, 68 | }, 69 | { 70 | name: 'Week', 71 | padding: '1m', 72 | step: '7d', 73 | date_format: 'YYYY-MM-DD', 74 | column_width: 140, 75 | lower_text: formatWeek, 76 | upper_text: (d, ld, lang) => 77 | !ld || d.getMonth() !== ld.getMonth() 78 | ? date_utils.format(d, 'MMMM', lang) 79 | : '', 80 | thick_line: (d) => d.getDate() >= 1 && d.getDate() <= 7, 81 | upper_text_frequency: 4, 82 | }, 83 | { 84 | name: 'Month', 85 | padding: '2m', 86 | step: '1m', 87 | column_width: 120, 88 | date_format: 'YYYY-MM', 89 | lower_text: 'MMMM', 90 | upper_text: (d, ld, lang) => 91 | !ld || d.getFullYear() !== ld.getFullYear() 92 | ? date_utils.format(d, 'YYYY', lang) 93 | : '', 94 | thick_line: (d) => d.getMonth() % 3 === 0, 95 | snap_at: '7d', 96 | }, 97 | { 98 | name: 'Year', 99 | padding: '2y', 100 | step: '1y', 101 | column_width: 120, 102 | date_format: 'YYYY', 103 | upper_text: (d, ld, lang) => 104 | !ld || getDecade(d) !== getDecade(ld) ? getDecade(d) : '', 105 | lower_text: 'YYYY', 106 | snap_at: '30d', 107 | }, 108 | ]; 109 | 110 | const DEFAULT_OPTIONS = { 111 | arrow_curve: 5, 112 | auto_move_label: false, 113 | bar_corner_radius: 3, 114 | bar_height: 30, 115 | container_height: 'auto', 116 | column_width: null, 117 | date_format: 'YYYY-MM-DD HH:mm', 118 | upper_header_height: 45, 119 | lower_header_height: 30, 120 | snap_at: null, 121 | infinite_padding: true, 122 | holidays: { 'var(--g-weekend-highlight-color)': 'weekend' }, 123 | ignore: [], 124 | language: 'en', 125 | lines: 'both', 126 | move_dependencies: true, 127 | padding: 18, 128 | popup: (ctx) => { 129 | ctx.set_title(ctx.task.name); 130 | if (ctx.task.description) ctx.set_subtitle(ctx.task.description); 131 | else ctx.set_subtitle(''); 132 | 133 | const start_date = date_utils.format( 134 | ctx.task._start, 135 | 'MMM D', 136 | ctx.chart.options.language, 137 | ); 138 | const end_date = date_utils.format( 139 | date_utils.add(ctx.task._end, -1, 'second'), 140 | 'MMM D', 141 | ctx.chart.options.language, 142 | ); 143 | 144 | ctx.set_details( 145 | ${start_date} - ${end_date} (${ctx.task.actual_duration} days${ctx.task.ignored_duration ? ' + ' + ctx.task.ignored_duration + ' excluded' : ''})<br/>Progress: ${Math.floor(ctx.task.progress * 100) / 100}%, 146 | ); 147 | }, 148 | popup_on: 'click', 149 | readonly_progress: false, 150 | readonly_dates: false, 151 | readonly: false, 152 | scroll_to: 'today', 153 | show_expected_progress: false, 154 | today_button: true, 155 | view_mode: 'Day', 156 | view_mode_select: false, 157 | view_modes: DEFAULT_VIEW_MODES, 158 | }; 159 | 160 | export { DEFAULT_OPTIONS, DEFAULT_VIEW_MODES }; 161 |


/src/index.js:

1 | import date_utils from './date_utils'; 2 | import { $, createSVG } from './svg_utils'; 3 | 4 | import Arrow from './arrow'; 5 | import Bar from './bar'; 6 | import Popup from './popup'; 7 | 8 | import { DEFAULT_OPTIONS, DEFAULT_VIEW_MODES } from './defaults'; 9 | 10 | import './styles/gantt.css'; 11 | 12 | export default class Gantt { 13 | constructor(wrapper, tasks, options) { 14 | this.setup_wrapper(wrapper); 15 | this.setup_options(options); 16 | this.setup_tasks(tasks); 17 | this.change_view_mode(); 18 | this.bind_events(); 19 | } 20 | 21 | setup_wrapper(element) { 22 | let svg_element, wrapper_element; 23 | 24 | // CSS Selector is passed 25 | if (typeof element === 'string') { 26 | let el = document.querySelector(element); 27 | if (!el) { 28 | throw new ReferenceError( 29 | CSS selector "${element}" could not be found in DOM, 30 | ); 31 | } 32 | element = el; 33 | } 34 | 35 | // get the SVGElement 36 | if (element instanceof HTMLElement) { 37 | wrapper_element = element; 38 | svg_element = element.querySelector('svg'); 39 | } else if (element instanceof SVGElement) { 40 | svg_element = element; 41 | } else { 42 | throw new TypeError( 43 | 'Frappe Gantt only supports usage of a string CSS selector,' + 44 | " HTML DOM element or SVG DOM element for the 'element' parameter", 45 | ); 46 | } 47 | 48 | // svg element 49 | if (!svg_element) { 50 | // create it 51 | this.$svg = createSVG('svg', { 52 | append_to: wrapper_element, 53 | class: 'gantt', 54 | }); 55 | } else { 56 | this.$svg = svg_element; 57 | this.$svg.classList.add('gantt'); 58 | } 59 | 60 | // wrapper element 61 | this.$container = this.create_el({ 62 | classes: 'gantt-container', 63 | append_to: this.$svg.parentElement, 64 | }); 65 | 66 | this.$container.appendChild(this.$svg); 67 | this.$popup_wrapper = this.create_el({ 68 | classes: 'popup-wrapper', 69 | append_to: this.$container, 70 | }); 71 | } 72 | 73 | setup_options(options) { 74 | this.original_options = options; 75 | this.options = { ...DEFAULT_OPTIONS, ...options }; 76 | const CSS_VARIABLES = { 77 | 'grid-height': 'container_height', 78 | 'bar-height': 'bar_height', 79 | 'lower-header-height': 'lower_header_height', 80 | 'upper-header-height': 'upper_header_height', 81 | }; 82 | for (let name in CSS_VARIABLES) { 83 | let setting = this.options[CSS_VARIABLES[name]]; 84 | if (setting !== 'auto') 85 | this.$container.style.setProperty( 86 | '--gv-' + name, 87 | setting + 'px', 88 | ); 89 | } 90 | 91 | this.config = { 92 | ignored_dates: [], 93 | ignored_positions: [], 94 | extend_by_units: 10, 95 | }; 96 | 97 | if (typeof this.options.ignore !== 'function') { 98 | if (typeof this.options.ignore === 'string') 99 | this.options.ignore = [this.options.ignord]; 100 | for (let option of this.options.ignore) { 101 | if (typeof option === 'function') { 102 | this.config.ignored_function = option; 103 | continue; 104 | } 105 | if (typeof option === 'string') { 106 | if (option === 'weekend') 107 | this.config.ignored_function = (d) => 108 | d.getDay() == 6 || d.getDay() == 0; 109 | else this.config.ignored_dates.push(new Date(option + ' ')); 110 | } 111 | } 112 | } else { 113 | this.config.ignored_function = this.options.ignore; 114 | } 115 | } 116 | 117 | update_options(options) { 118 | this.setup_options({ ...this.original_options, ...options }); 119 | this.change_view_mode(undefined, true); 120 | } 121 | 122 | setup_tasks(tasks) { 123 | this.tasks = tasks 124 | .map((task, i) => { 125 | if (!task.start) { 126 | console.error( 127 | task "${task.id}" doesn't have a start date, 128 | ); 129 | return false; 130 | } 131 | 132 | task._start = date_utils.parse(task.start); 133 | if (task.end === undefined && task.duration !== undefined) { 134 | task.end = task._start; 135 | let durations = task.duration.split(' '); 136 | 137 | durations.forEach((tmpDuration) => { 138 | let { duration, scale } = 139 | date_utils.parse_duration(tmpDuration); 140 | task.end = date_utils.add(task.end, duration, scale); 141 | }); 142 | } 143 | if (!task.end) { 144 | console.error(task "${task.id}" doesn't have an end date); 145 | return false; 146 | } 147 | task.end = date_utils.parse(task.end); 148 | 149 | let diff = date_utils.diff(task.end, task.start, 'year'); 150 | if (diff < 0) { 151 | console.error( 152 | start of task can't be after end of task: in task "${task.id}", 153 | ); 154 | return false; 155 | } 156 | 157 | // make task invalid if duration too large 158 | if (date_utils.diff(task.end, task.start, 'year') > 10) { 159 | console.error( 160 | the duration of task "${task.id}" is too long (above ten years), 161 | ); 162 | return false; 163 | } 164 | 165 | // cache index 166 | task.index = i; 167 | 168 | // if hours is not set, assume the last day is full day 169 | // e.g: 2018-09-09 becomes 2018-09-09 23:59:59 170 | const task_end_values = date_utils.get_date_values(task.end); 171 | if (task_end_values.slice(3).every((d) => d === 0)) { 172 | task.end = date_utils.add(task.end, 24, 'hour'); 173 | } 174 | 175 | // dependencies 176 | if ( 177 | typeof task.dependencies === 'string' || 178 | !task.dependencies 179 | ) { 180 | let deps = []; 181 | if (task.dependencies) { 182 | deps = task.dependencies 183 | .split(',') 184 | .map((d) => d.trim().replaceAll(' ', '')) 185 | .filter((d) => d); 186 | } 187 | task.dependencies = deps; 188 | } 189 | 190 | // uids 191 | if (!task.id) { 192 | task.id = generate_id(task); 193 | } else if (typeof task.id === 'string') { 194 | task.id = task.id.replaceAll(' ', ''); 195 | } else { 196 | task.id = ${task.id}; 197 | } 198 | 199 | return task; 200 | }) 201 | .filter((t) => t); 202 | this.setup_dependencies(); 203 | } 204 | 205 | setup_dependencies() { 206 | this.dependency_map = {}; 207 | for (let t of this.tasks) { 208 | for (let d of t.dependencies) { 209 | this.dependency_map[d] = this.dependency_map[d] || []; 210 | this.dependency_map[d].push(t.id); 211 | } 212 | } 213 | } 214 | 215 | refresh(tasks) { 216 | this.setup_tasks(tasks); 217 | this.change_view_mode(); 218 | } 219 | 220 | update_task(id, new_details) { 221 | let task = this.tasks.find((t) => t.id === id); 222 | let bar = this.bars[task.index]; 223 | Object.assign(task, new_details); 224 | bar.refresh(); 225 | } 226 | 227 | change_view_mode(mode = this.options.view_mode, maintain_pos = false) { 228 | if (typeof mode === 'string') { 229 | mode = this.options.view_modes.find((d) => d.name === mode); 230 | } 231 | let old_pos, old_scroll_op; 232 | if (maintain_pos) { 233 | old_pos = this.$container.scrollLeft; 234 | old_scroll_op = this.options.scroll_to; 235 | this.options.scroll_to = null; 236 | } 237 | this.options.view_mode = mode.name; 238 | this.config.view_mode = mode; 239 | this.update_view_scale(mode); 240 | this.setup_dates(maintain_pos); 241 | this.render(); 242 | if (maintain_pos) { 243 | this.$container.scrollLeft = old_pos; 244 | this.options.scroll_to = old_scroll_op; 245 | } 246 | this.trigger_event('view_change', [mode]); 247 | } 248 | 249 | update_view_scale(mode) { 250 | let { duration, scale } = date_utils.parse_duration(mode.step); 251 | this.config.step = duration; 252 | this.config.unit = scale; 253 | this.config.column_width = 254 | this.options.column_width || mode.column_width || 45; 255 | this.$container.style.setProperty( 256 | '--gv-column-width', 257 | this.config.column_width + 'px', 258 | ); 259 | this.config.header_height = 260 | this.options.lower_header_height + 261 | this.options.upper_header_height + 262 | 10; 263 | } 264 | 265 | setup_dates(refresh = false) { 266 | this.setup_gantt_dates(refresh); 267 | this.setup_date_values(); 268 | } 269 | 270 | setup_gantt_dates(refresh) { 271 | let gantt_start, gantt_end; 272 | if (!this.tasks.length) { 273 | gantt_start = new Date(); 274 | gantt_end = new Date(); 275 | } 276 | 277 | for (let task of this.tasks) { 278 | if (!gantt_start || task.start < gantt_start) { 279 | gantt_start = task.start; 280 | } 281 | if (!gantt_end || task.end > gantt_end) { 282 | gantt_end = task.end; 283 | } 284 | } 285 | 286 | gantt_start = date_utils.start_of(gantt_start, this.config.unit); 287 | gantt_end = date_utils.start_of(gantt_end, this.config.unit); 288 | 289 | if (!refresh) { 290 | if (!this.options.infinite_padding) { 291 | if (typeof this.config.view_mode.padding === 'string') 292 | this.config.view_mode.padding = [ 293 | this.config.view_mode.padding, 294 | this.config.view_mode.padding, 295 | ]; 296 | 297 | let [padding_start, padding_end] = 298 | this.config.view_mode.padding.map( 299 | date_utils.parse_duration, 300 | ); 301 | this.gantt_start = date_utils.add( 302 | gantt_start, 303 | -padding_start.duration, 304 | padding_start.scale, 305 | ); 306 | this.gantt_end = date_utils.add( 307 | gantt_end, 308 | padding_end.duration, 309 | padding_end.scale, 310 | ); 311 | } else { 312 | this.gantt_start = date_utils.add( 313 | gantt_start, 314 | -this.config.extend_by_units * 3, 315 | this.config.unit, 316 | ); 317 | this.gantt_end = date_utils.add( 318 | gantt_end, 319 | this.config.extend_by_units * 3, 320 | this.config.unit, 321 | ); 322 | } 323 | } 324 | this.config.date_format = 325 | this.config.view_mode.date_format || this.options.date_format; 326 | this.gantt_start.setHours(0, 0, 0, 0); 327 | } 328 | 329 | setup_date_values() { 330 | let cur_date = this.gantt_start; 331 | this.dates = [cur_date]; 332 | 333 | while (cur_date < this.gantt_end) { 334 | cur_date = date_utils.add( 335 | cur_date, 336 | this.config.step, 337 | this.config.unit, 338 | ); 339 | this.dates.push(cur_date); 340 | } 341 | } 342 | 343 | bind_events() { 344 | this.bind_grid_click(); 345 | this.bind_holiday_labels(); 346 | this.bind_bar_events(); 347 | } 348 | 349 | render() { 350 | this.clear(); 351 | this.setup_layers(); 352 | this.make_grid(); 353 | this.make_dates(); 354 | this.make_grid_extras(); 355 | this.make_bars(); 356 | this.make_arrows(); 357 | this.map_arrows_on_bars(); 358 | this.set_dimensions(); 359 | this.set_scroll_position(this.options.scroll_to); 360 | } 361 | 362 | setup_layers() { 363 | this.layers = {}; 364 | const layers = ['grid', 'arrow', 'progress', 'bar']; 365 | // make group layers 366 | for (let layer of layers) { 367 | this.layers[layer] = createSVG('g', { 368 | class: layer, 369 | append_to: this.$svg, 370 | }); 371 | } 372 | this.$extras = this.create_el({ 373 | classes: 'extras', 374 | append_to: this.$container, 375 | }); 376 | this.$adjust = this.create_el({ 377 | classes: 'adjust hide', 378 | append_to: this.$extras, 379 | type: 'button', 380 | }); 381 | this.$adjust.innerHTML = '←'; 382 | } 383 | 384 | make_grid() { 385 | this.make_grid_background(); 386 | this.make_grid_rows(); 387 | this.make_grid_header(); 388 | this.make_side_header(); 389 | } 390 | 391 | make_grid_extras() { 392 | this.make_grid_highlights(); 393 | this.make_grid_ticks(); 394 | } 395 | 396 | make_grid_background() { 397 | const grid_width = this.dates.length * this.config.column_width; 398 | const grid_height = Math.max( 399 | this.config.header_height + 400 | this.options.padding + 401 | (this.options.bar_height + this.options.padding) * 402 | this.tasks.length - 403 | 10, 404 | this.options.container_height !== 'auto' 405 | ? this.options.container_height 406 | : 0, 407 | ); 408 | 409 | createSVG('rect', { 410 | x: 0, 411 | y: 0, 412 | width: grid_width, 413 | height: grid_height, 414 | class: 'grid-background', 415 | append_to: this.$svg, 416 | }); 417 | 418 | $.attr(this.$svg, { 419 | height: grid_height, 420 | width: '100%', 421 | }); 422 | this.grid_height = grid_height; 423 | if (this.options.container_height === 'auto') 424 | this.$container.style.height = grid_height + 'px'; 425 | } 426 | 427 | make_grid_rows() { 428 | const rows_layer = createSVG('g', { append_to: this.layers.grid }); 429 | 430 | const row_width = this.dates.length * this.config.column_width; 431 | const row_height = this.options.bar_height + this.options.padding; 432 | 433 | let y = this.config.header_height; 434 | for ( 435 | let y = this.config.header_height; 436 | y < this.grid_height; 437 | y += row_height 438 | ) { 439 | createSVG('rect', { 440 | x: 0, 441 | y, 442 | width: row_width, 443 | height: row_height, 444 | class: 'grid-row', 445 | append_to: rows_layer, 446 | }); 447 | } 448 | } 449 | 450 | make_grid_header() { 451 | this.$header = this.create_el({ 452 | width: this.dates.length * this.config.column_width, 453 | classes: 'grid-header', 454 | append_to: this.$container, 455 | }); 456 | 457 | this.$upper_header = this.create_el({ 458 | classes: 'upper-header', 459 | append_to: this.$header, 460 | }); 461 | this.$lower_header = this.create_el({ 462 | classes: 'lower-header', 463 | append_to: this.$header, 464 | }); 465 | } 466 | 467 | make_side_header() { 468 | this.$side_header = this.create_el({ classes: 'side-header' }); 469 | this.$upper_header.prepend(this.$side_header); 470 | 471 | // Create view mode change select 472 | if (this.options.view_mode_select) { 473 | const $select = document.createElement('select'); 474 | $select.classList.add('viewmode-select'); 475 | 476 | const $el = document.createElement('option'); 477 | $el.selected = true; 478 | $el.disabled = true; 479 | $el.textContent = 'Mode'; 480 | $select.appendChild($el); 481 | 482 | for (const mode of this.options.view_modes) { 483 | const $option = document.createElement('option'); 484 | $option.value = mode.name; 485 | $option.textContent = mode.name; 486 | if (mode.name === this.config.view_mode.name) 487 | $option.selected = true; 488 | $select.appendChild($option); 489 | } 490 | 491 | $select.addEventListener( 492 | 'change', 493 | function () { 494 | this.change_view_mode($select.value, true); 495 | }.bind(this), 496 | ); 497 | this.$side_header.appendChild($select); 498 | } 499 | 500 | // Create today button 501 | if (this.options.today_button) { 502 | let $today_button = document.createElement('button'); 503 | $today_button.classList.add('today-button'); 504 | $today_button.textContent = 'Today'; 505 | $today_button.onclick = this.scroll_current.bind(this); 506 | this.$side_header.prepend($today_button); 507 | this.$today_button = $today_button; 508 | } 509 | } 510 | 511 | make_grid_ticks() { 512 | if (this.options.lines === 'none') return; 513 | let tick_x = 0; 514 | let tick_y = this.config.header_height; 515 | let tick_height = this.grid_height - this.config.header_height; 516 | 517 | let $lines_layer = createSVG('g', { 518 | class: 'lines_layer', 519 | append_to: this.layers.grid, 520 | }); 521 | 522 | let row_y = this.config.header_height; 523 | 524 | const row_width = this.dates.length * this.config.column_width; 525 | const row_height = this.options.bar_height + this.options.padding; 526 | if (this.options.lines !== 'vertical') { 527 | for ( 528 | let y = this.config.header_height; 529 | y < this.grid_height; 530 | y += row_height 531 | ) { 532 | createSVG('line', { 533 | x1: 0, 534 | y1: row_y + row_height, 535 | x2: row_width, 536 | y2: row_y + row_height, 537 | class: 'row-line', 538 | append_to: $lines_layer, 539 | }); 540 | row_y += row_height; 541 | } 542 | } 543 | if (this.options.lines === 'horizontal') return; 544 | 545 | for (let date of this.dates) { 546 | let tick_class = 'tick'; 547 | if ( 548 | this.config.view_mode.thick_line && 549 | this.config.view_mode.thick_line(date) 550 | ) { 551 | tick_class += ' thick'; 552 | } 553 | 554 | createSVG('path', { 555 | d: M ${tick_x} ${tick_y} v ${tick_height}, 556 | class: tick_class, 557 | append_to: this.layers.grid, 558 | }); 559 | 560 | if (this.view_is('month')) { 561 | tick_x += 562 | (date_utils.get_days_in_month(date) * 563 | this.config.column_width) / 564 | 30; 565 | } else if (this.view_is('year')) { 566 | tick_x += 567 | (date_utils.get_days_in_year(date) * 568 | this.config.column_width) / 569 | 365; 570 | } else { 571 | tick_x += this.config.column_width; 572 | } 573 | } 574 | } 575 | 576 | highlight_holidays() { 577 | let labels = {}; 578 | if (!this.options.holidays) return; 579 | 580 | for (let color in this.options.holidays) { 581 | let check_highlight = this.options.holidays[color]; 582 | if (check_highlight === 'weekend') 583 | check_highlight = (d) => d.getDay() === 0 || d.getDay() === 6; 584 | let extra_func; 585 | 586 | if (typeof check_highlight === 'object') { 587 | let f = check_highlight.find((k) => typeof k === 'function'); 588 | if (f) { 589 | extra_func = f; 590 | } 591 | if (this.options.holidays.name) { 592 | let dateObj = new Date(check_highlight.date + ' '); 593 | check_highlight = (d) => dateObj.getTime() === d.getTime(); 594 | labels[dateObj] = check_highlight.name; 595 | } else { 596 | check_highlight = (d) => 597 | this.options.holidays[color] 598 | .filter((k) => typeof k !== 'function') 599 | .map((k) => { 600 | if (k.name) { 601 | let dateObj = new Date(k.date + ' '); 602 | labels[dateObj] = k.name; 603 | return dateObj.getTime(); 604 | } 605 | return new Date(k + ' ').getTime(); 606 | }) 607 | .includes(d.getTime()); 608 | } 609 | } 610 | for ( 611 | let d = new Date(this.gantt_start); 612 | d <= this.gantt_end; 613 | d.setDate(d.getDate() + 1) 614 | ) { 615 | if ( 616 | this.config.ignored_dates.find( 617 | (k) => k.getTime() == d.getTime(), 618 | ) || 619 | (this.config.ignored_function && 620 | this.config.ignored_function(d)) 621 | ) 622 | continue; 623 | if (check_highlight(d) || (extra_func && extra_func(d))) { 624 | const x = 625 | (date_utils.diff( 626 | d, 627 | this.gantt_start, 628 | this.config.unit, 629 | ) / 630 | this.config.step) * 631 | this.config.column_width; 632 | const height = this.grid_height - this.config.header_height; 633 | const d_formatted = date_utils 634 | .format(d, 'YYYY-MM-DD', this.options.language) 635 | .replace(' ', ''); 636 | 637 | if (labels[d]) { 638 | let label = this.create_el({ 639 | classes: 'holiday-label ' + 'label' + d_formatted, 640 | append_to: this.$extras, 641 | }); 642 | label.textContent = labels[d]; 643 | } 644 | createSVG('rect', { 645 | x: Math.round(x), 646 | y: this.config.header_height, 647 | width: 648 | this.config.column_width / 649 | date_utils.convert_scales( 650 | this.config.view_mode.step, 651 | 'day', 652 | ), 653 | height, 654 | class: 'holiday-highlight ' + d_formatted, 655 | style: fill: ${color};, 656 | append_to: this.layers.grid, 657 | }); 658 | } 659 | } 660 | } 661 | } 662 | 663 | /** 664 | * Compute the horizontal x-axis distance and associated date for the current date and view. 665 | * 666 | * @returns Object containing the x-axis distance and date of the current date, or null if the current date is out of the gantt range. 667 | */ 668 | highlight_current() { 669 | const res = this.get_closest_date(); 670 | if (!res) return; 671 | 672 | const [, el] = res; 673 | el.classList.add('current-date-highlight'); 674 | 675 | const diff_in_units = date_utils.diff( 676 | new Date(), 677 | this.gantt_start, 678 | this.config.unit, 679 | ); 680 | 681 | const left = 682 | (diff_in_units / this.config.step) * this.config.column_width; 683 | 684 | this.$current_highlight = this.create_el({ 685 | top: this.config.header_height, 686 | left, 687 | height: this.grid_height - this.config.header_height, 688 | classes: 'current-highlight', 689 | append_to: this.$container, 690 | }); 691 | this.$current_ball_highlight = this.create_el({ 692 | top: this.config.header_height - 6, 693 | left: left - 2.5, 694 | width: 6, 695 | height: 6, 696 | classes: 'current-ball-highlight', 697 | append_to: this.$header, 698 | }); 699 | } 700 | 701 | make_grid_highlights() { 702 | this.highlight_holidays(); 703 | this.config.ignored_positions = []; 704 | 705 | const height = 706 | (this.options.bar_height + this.options.padding) * 707 | this.tasks.length; 708 | this.layers.grid.innerHTML += <pattern id="diagonalHatch" patternUnits="userSpaceOnUse" width="4" height="4"> 709 | <path d="M-1,1 l2,-2 710 | M0,4 l4,-4 711 | M3,5 l2,-2" 712 | style="stroke:grey; stroke-width:0.3" /> 713 | </pattern>; 714 | 715 | for ( 716 | let d = new Date(this.gantt_start); 717 | d <= this.gantt_end; 718 | d.setDate(d.getDate() + 1) 719 | ) { 720 | if ( 721 | !this.config.ignored_dates.find( 722 | (k) => k.getTime() == d.getTime(), 723 | ) && 724 | (!this.config.ignored_function || 725 | !this.config.ignored_function(d)) 726 | ) 727 | continue; 728 | let diff = 729 | date_utils.convert_scales( 730 | date_utils.diff(d, this.gantt_start) + 'd', 731 | this.config.unit, 732 | ) / this.config.step; 733 | 734 | this.config.ignored_positions.push(diff * this.config.column_width); 735 | createSVG('rect', { 736 | x: diff * this.config.column_width, 737 | y: this.config.header_height, 738 | width: this.config.column_width, 739 | height: height, 740 | class: 'ignored-bar', 741 | style: 'fill: url(#diagonalHatch);', 742 | append_to: this.$svg, 743 | }); 744 | } 745 | 746 | const highlightDimensions = this.highlight_current( 747 | this.config.view_mode, 748 | ); 749 | 750 | if (!highlightDimensions) return; 751 | } 752 | 753 | create_el({ left, top, width, height, id, classes, append_to, type }) { 754 | let $el = document.createElement(type || 'div'); 755 | for (let cls of classes.split(' ')) $el.classList.add(cls); 756 | $el.style.top = top + 'px'; 757 | $el.style.left = left + 'px'; 758 | if (id) $el.id = id; 759 | if (width) $el.style.width = width + 'px'; 760 | if (height) $el.style.height = height + 'px'; 761 | if (append_to) append_to.appendChild($el); 762 | return $el; 763 | } 764 | 765 | make_dates() { 766 | this.get_dates_to_draw().forEach((date, i) => { 767 | if (date.lower_text) { 768 | let $lower_text = this.create_el({ 769 | left: date.x, 770 | top: date.lower_y, 771 | classes: 'lower-text date' + sanitize(date.formatted_date), 772 | append_to: this.$lower_header, 773 | }); 774 | $lower_text.innerText = date.lower_text; 775 | } 776 | 777 | if (date.upper_text) { 778 | let $upper_text = this.create_el({ 779 | left: date.x, 780 | top: date.upper_y, 781 | classes: 'upper-text', 782 | append_to: this.$upper_header, 783 | }); 784 | $upper_text.innerText = date.upper_text; 785 | } 786 | }); 787 | this.upperTexts = Array.from( 788 | this.$container.querySelectorAll('.upper-text'), 789 | ); 790 | } 791 | 792 | get_dates_to_draw() { 793 | let last_date_info = null; 794 | const dates = this.dates.map((date, i) => { 795 | const d = this.get_date_info(date, last_date_info, i); 796 | last_date_info = d; 797 | return d; 798 | }); 799 | return dates; 800 | } 801 | 802 | get_date_info(date, last_date_info) { 803 | let last_date = last_date_info ? last_date_info.date : null; 804 | 805 | let column_width = this.config.column_width; 806 | 807 | const x = last_date_info 808 | ? last_date_info.x + last_date_info.column_width 809 | : 0; 810 | 811 | let upper_text = this.config.view_mode.upper_text; 812 | let lower_text = this.config.view_mode.lower_text; 813 | 814 | if (!upper_text) { 815 | this.config.view_mode.upper_text = () => ''; 816 | } else if (typeof upper_text === 'string') { 817 | this.config.view_mode.upper_text = (date) => 818 | date_utils.format(date, upper_text, this.options.language); 819 | } 820 | 821 | if (!lower_text) { 822 | this.config.view_mode.lower_text = () => ''; 823 | } else if (typeof lower_text === 'string') { 824 | this.config.view_mode.lower_text = (date) => 825 | date_utils.format(date, lower_text, this.options.language); 826 | } 827 | 828 | return { 829 | date, 830 | formatted_date: sanitize( 831 | date_utils.format( 832 | date, 833 | this.config.date_format, 834 | this.options.language, 835 | ), 836 | ), 837 | column_width: this.config.column_width, 838 | x, 839 | upper_text: this.config.view_mode.upper_text( 840 | date, 841 | last_date, 842 | this.options.language, 843 | ), 844 | lower_text: this.config.view_mode.lower_text( 845 | date, 846 | last_date, 847 | this.options.language, 848 | ), 849 | upper_y: 17, 850 | lower_y: this.options.upper_header_height + 5, 851 | }; 852 | } 853 | 854 | make_bars() { 855 | this.bars = this.tasks.map((task) => { 856 | const bar = new Bar(this, task); 857 | this.layers.bar.appendChild(bar.group); 858 | return bar; 859 | }); 860 | } 861 | 862 | make_arrows() { 863 | this.arrows = []; 864 | for (let task of this.tasks) { 865 | let arrows = []; 866 | arrows = task.dependencies 867 | .map((task_id) => { 868 | const dependency = this.get_task(task_id); 869 | if (!dependency) return; 870 | const arrow = new Arrow( 871 | this, 872 | this.bars[dependency.index], // from_task 873 | this.bars[task.index], // to_task 874 | ); 875 | this.layers.arrow.appendChild(arrow.element); 876 | return arrow; 877 | }) 878 | .filter(Boolean); // filter falsy values 879 | this.arrows = this.arrows.concat(arrows); 880 | } 881 | } 882 | 883 | map_arrows_on_bars() { 884 | for (let bar of this.bars) { 885 | bar.arrows = this.arrows.filter((arrow) => { 886 | return ( 887 | arrow.from_task.task.id === bar.task.id || 888 | arrow.to_task.task.id === bar.task.id 889 | ); 890 | }); 891 | } 892 | } 893 | 894 | set_dimensions() { 895 | const { width: cur_width } = this.$svg.getBoundingClientRect(); 896 | const actual_width = this.$svg.querySelector('.grid .grid-row') 897 | ? this.$svg.querySelector('.grid .grid-row').getAttribute('width') 898 | : 0; 899 | if (cur_width < actual_width) { 900 | this.$svg.setAttribute('width', actual_width); 901 | } 902 | } 903 | 904 | set_scroll_position(date) { 905 | if (this.options.infinite_padding && (!date || date === 'start')) { 906 | let [min_start, ...] = this.get_start_end_positions(); 907 | this.$container.scrollLeft = min_start; 908 | return; 909 | } 910 | if (!date || date === 'start') { 911 | date = this.gantt_start; 912 | } else if (date === 'end') { 913 | date = this.gantt_end; 914 | } else if (date === 'today') { 915 | return this.scroll_current(); 916 | } else if (typeof date === 'string') { 917 | date = date_utils.parse(date); 918 | } 919 | 920 | // Weird bug where infinite padding results in one day offset in scroll 921 | // Related to header-body displacement 922 | const units_since_first_task = date_utils.diff( 923 | date, 924 | this.gantt_start, 925 | this.config.unit, 926 | ); 927 | const scroll_pos = 928 | (units_since_first_task / this.config.step) * 929 | this.config.column_width; 930 | 931 | this.$container.scrollTo({ 932 | left: scroll_pos - this.config.column_width / 6, 933 | behavior: 'smooth', 934 | }); 935 | 936 | // Calculate current scroll position's upper text 937 | if (this.$current) { 938 | this.$current.classList.remove('current-upper'); 939 | } 940 | 941 | this.current_date = date_utils.add( 942 | this.gantt_start, 943 | this.$container.scrollLeft / this.config.column_width, 944 | this.config.unit, 945 | ); 946 | 947 | let current_upper = this.config.view_mode.upper_text( 948 | this.current_date, 949 | null, 950 | this.options.language, 951 | ); 952 | let $el = this.upperTexts.find( 953 | (el) => el.textContent === current_upper, 954 | ); 955 | 956 | // Recalculate 957 | this.current_date = date_utils.add( 958 | this.gantt_start, 959 | (this.$container.scrollLeft + $el.clientWidth) / 960 | this.config.column_width, 961 | this.config.unit, 962 | ); 963 | current_upper = this.config.view_mode.upper_text( 964 | this.current_date, 965 | null, 966 | this.options.language, 967 | ); 968 | $el = this.upperTexts.find((el) => el.textContent === current_upper); 969 | $el.classList.add('current-upper'); 970 | this.$current = $el; 971 | } 972 | 973 | scroll_current() { 974 | let res = this.get_closest_date(); 975 | if (res) this.set_scroll_position(res[0]); 976 | } 977 | 978 | get_closest_date() { 979 | let now = new Date(); 980 | if (now < this.gantt_start || now > this.gantt_end) return null; 981 | 982 | let current = new Date(), 983 | el = this.$container.querySelector( 984 | '.date' + 985 | sanitize( 986 | date_utils.format( 987 | current, 988 | this.config.date_format, 989 | this.options.language, 990 | ), 991 | ), 992 | ); 993 | 994 | // safety check to prevent infinite loop 995 | let c = 0; 996 | while (!el && c < this.config.step) { 997 | current = date_utils.add(current, -1, this.config.unit); 998 | el = this.$container.querySelector( 999 | '.date' + 1000 | sanitize( 1001 | date_utils.format( 1002 | current, 1003 | this.config.date_format, 1004 | this.options.language, 1005 | ), 1006 | ), 1007 | ); 1008 | c++; 1009 | } 1010 | return [ 1011 | new Date( 1012 | date_utils.format( 1013 | current, 1014 | this.config.date_format, 1015 | this.options.language, 1016 | ) + ' ', 1017 | ), 1018 | el, 1019 | ]; 1020 | } 1021 | 1022 | bind_grid_click() { 1023 | $.on( 1024 | this.$container, 1025 | 'click', 1026 | '.grid-row, .grid-header, .ignored-bar, .holiday-highlight', 1027 | () => { 1028 | this.unselect_all(); 1029 | this.hide_popup(); 1030 | }, 1031 | ); 1032 | } 1033 | 1034 | bind_holiday_labels() { 1035 | const $highlights = 1036 | this.$container.querySelectorAll('.holiday-highlight'); 1037 | for (let h of $highlights) { 1038 | const label = this.$container.querySelector( 1039 | '.label' + h.classList[1], 1040 | ); 1041 | if (!label) continue; 1042 | let timeout; 1043 | h.onmouseenter = (e) => { 1044 | timeout = setTimeout(() => { 1045 | label.classList.add('show'); 1046 | label.style.left = (e.offsetX || e.layerX) + 'px'; 1047 | label.style.top = (e.offsetY || e.layerY) + 'px'; 1048 | }, 300); 1049 | }; 1050 | 1051 | h.onmouseleave = (e) => { 1052 | clearTimeout(timeout); 1053 | label.classList.remove('show'); 1054 | }; 1055 | } 1056 | } 1057 | 1058 | get_start_end_positions() { 1059 | if (!this.bars.length) return [0, 0, 0]; 1060 | let { x, width } = this.bars[0].group.getBBox(); 1061 | let min_start = x; 1062 | let max_start = x; 1063 | let max_end = x + width; 1064 | Array.prototype.forEach.call(this.bars, function ({ group }, i) { 1065 | let { x, width } = group.getBBox(); 1066 | if (x < min_start) min_start = x; 1067 | if (x > max_start) max_start = x; 1068 | if (x + width > max_end) max_end = x + width; 1069 | }); 1070 | return [min_start, max_start, max_end]; 1071 | } 1072 | 1073 | bind_bar_events() { 1074 | let is_dragging = false; 1075 | let x_on_start = 0; 1076 | let x_on_scroll_start = 0; 1077 | let y_on_start = 0; 1078 | let is_resizing_left = false; 1079 | let is_resizing_right = false; 1080 | let parent_bar_id = null; 1081 | let bars = []; // instanceof Bar 1082 | this.bar_being_dragged = null; 1083 | 1084 | const action_in_progress = () => 1085 | is_dragging || is_resizing_left || is_resizing_right; 1086 | 1087 | this.$svg.onclick = (e) => { 1088 | if (e.target.classList.contains('grid-row')) this.unselect_all(); 1089 | }; 1090 | 1091 | let pos = 0; 1092 | $.on(this.$svg, 'mousemove', '.bar-wrapper, .handle', (e) => { 1093 | if ( 1094 | this.bar_being_dragged === false && 1095 | Math.abs((e.offsetX || e.layerX) - pos) > 10 1096 | ) 1097 | this.bar_being_dragged = true; 1098 | }); 1099 | 1100 | $.on(this.$svg, 'mousedown', '.bar-wrapper, .handle', (e, element) => { 1101 | const bar_wrapper = $.closest('.bar-wrapper', element); 1102 | if (element.classList.contains('left')) { 1103 | is_resizing_left = true; 1104 | element.classList.add('visible'); 1105 | } else if (element.classList.contains('right')) { 1106 | is_resizing_right = true; 1107 | element.classList.add('visible'); 1108 | } else if (element.classList.contains('bar-wrapper')) { 1109 | is_dragging = true; 1110 | } 1111 | 1112 | if (this.popup) this.popup.hide(); 1113 | 1114 | x_on_start = e.offsetX || e.layerX; 1115 | y_on_start = e.offsetY || e.layerY; 1116 | 1117 | parent_bar_id = bar_wrapper.getAttribute('data-id'); 1118 | let ids; 1119 | if (this.options.move_dependencies) { 1120 | ids = [ 1121 | parent_bar_id, 1122 | ...this.get_all_dependent_tasks(parent_bar_id), 1123 | ]; 1124 | } else { 1125 | ids = [parent_bar_id]; 1126 | } 1127 | bars = ids.map((id) => this.get_bar(id)); 1128 | 1129 | this.bar_being_dragged = false; 1130 | pos = x_on_start; 1131 | 1132 | bars.forEach((bar) => { 1133 | const $bar = bar.$bar; 1134 | $bar.ox = $bar.getX(); 1135 | $bar.oy = $bar.getY(); 1136 | $bar.owidth = $bar.getWidth(); 1137 | $bar.finaldx = 0; 1138 | }); 1139 | }); 1140 | 1141 | if (this.options.infinite_padding) { 1142 | let extended = false; 1143 | $.on(this.$container, 'mousewheel', (e) => { 1144 | let trigger = this.$container.scrollWidth / 2; 1145 | if (!extended && e.currentTarget.scrollLeft <= trigger) { 1146 | let old_scroll_left = e.currentTarget.scrollLeft; 1147 | extended = true; 1148 | 1149 | this.gantt_start = date_utils.add( 1150 | this.gantt_start, 1151 | -this.config.extend_by_units, 1152 | this.config.unit, 1153 | ); 1154 | this.setup_date_values(); 1155 | this.render(); 1156 | e.currentTarget.scrollLeft = 1157 | old_scroll_left + 1158 | this.config.column_width * this.config.extend_by_units; 1159 | setTimeout(() => (extended = false), 300); 1160 | } 1161 | 1162 | if ( 1163 | !extended && 1164 | e.currentTarget.scrollWidth - 1165 | (e.currentTarget.scrollLeft + 1166 | e.currentTarget.clientWidth) <= 1167 | trigger 1168 | ) { 1169 | let old_scroll_left = e.currentTarget.scrollLeft; 1170 | extended = true; 1171 | this.gantt_end = date_utils.add( 1172 | this.gantt_end, 1173 | this.config.extend_by_units, 1174 | this.config.unit, 1175 | ); 1176 | this.setup_date_values(); 1177 | this.render(); 1178 | e.currentTarget.scrollLeft = old_scroll_left; 1179 | setTimeout(() => (extended = false), 300); 1180 | } 1181 | }); 1182 | } 1183 | 1184 | $.on(this.$container, 'scroll', (e) => { 1185 | let localBars = []; 1186 | const ids = this.bars.map(({ group }) => 1187 | group.getAttribute('data-id'), 1188 | ); 1189 | let dx; 1190 | if (x_on_scroll_start) { 1191 | dx = e.currentTarget.scrollLeft - x_on_scroll_start; 1192 | } 1193 | 1194 | // Calculate current scroll position's upper text 1195 | this.current_date = date_utils.add( 1196 | this.gantt_start, 1197 | (e.currentTarget.scrollLeft / this.config.column_width) * 1198 | this.config.step, 1199 | this.config.unit, 1200 | ); 1201 | 1202 | let current_upper = this.config.view_mode.upper_text( 1203 | this.current_date, 1204 | null, 1205 | this.options.language, 1206 | ); 1207 | let $el = this.upperTexts.find( 1208 | (el) => el.textContent === current_upper, 1209 | ); 1210 | 1211 | // Recalculate for smoother experience 1212 | this.current_date = date_utils.add( 1213 | this.gantt_start, 1214 | ((e.currentTarget.scrollLeft + $el.clientWidth) / 1215 | this.config.column_width) * 1216 | this.config.step, 1217 | this.config.unit, 1218 | ); 1219 | current_upper = this.config.view_mode.upper_text( 1220 | this.current_date, 1221 | null, 1222 | this.options.language, 1223 | ); 1224 | $el = this.upperTexts.find( 1225 | (el) => el.textContent === current_upper, 1226 | ); 1227 | 1228 | if ($el !== this.$current) { 1229 | if (this.$current) 1230 | this.$current.classList.remove('current-upper'); 1231 | 1232 | $el.classList.add('current-upper'); 1233 | this.$current = $el; 1234 | } 1235 | 1236 | x_on_scroll_start = e.currentTarget.scrollLeft; 1237 | let [min_start, max_start, max_end] = 1238 | this.get_start_end_positions(); 1239 | 1240 | if (x_on_scroll_start > max_end + 100) { 1241 | this.$adjust.innerHTML = '←'; 1242 | this.$adjust.classList.remove('hide'); 1243 | this.$adjust.onclick = () => { 1244 | this.$container.scrollTo({ 1245 | left: max_start, 1246 | behavior: 'smooth', 1247 | }); 1248 | }; 1249 | } else if ( 1250 | x_on_scroll_start + e.currentTarget.offsetWidth < 1251 | min_start - 100 1252 | ) { 1253 | this.$adjust.innerHTML = '→'; 1254 | this.$adjust.classList.remove('hide'); 1255 | this.$adjust.onclick = () => { 1256 | this.$container.scrollTo({ 1257 | left: min_start, 1258 | behavior: 'smooth', 1259 | }); 1260 | }; 1261 | } else { 1262 | this.$adjust.classList.add('hide'); 1263 | } 1264 | 1265 | if (dx) { 1266 | localBars = ids.map((id) => this.get_bar(id)); 1267 | if (this.options.auto_move_label) { 1268 | localBars.forEach((bar) => { 1269 | bar.update_label_position_on_horizontal_scroll({ 1270 | x: dx, 1271 | sx: e.currentTarget.scrollLeft, 1272 | }); 1273 | }); 1274 | } 1275 | } 1276 | }); 1277 | 1278 | $.on(this.$svg, 'mousemove', (e) => { 1279 | if (!action_in_progress()) return; 1280 | const dx = (e.offsetX || e.layerX) - x_on_start; 1281 | 1282 | bars.forEach((bar) => { 1283 | const $bar = bar.$bar; 1284 | $bar.finaldx = this.get_snap_position(dx, $bar.ox); 1285 | this.hide_popup(); 1286 | if (is_resizing_left) { 1287 | if (parent_bar_id === bar.task.id) { 1288 | bar.update_bar_position({ 1289 | x: $bar.ox + $bar.finaldx, 1290 | width: $bar.owidth - $bar.finaldx, 1291 | }); 1292 | } else { 1293 | bar.update_bar_position({ 1294 | x: $bar.ox + $bar.finaldx, 1295 | }); 1296 | } 1297 | } else if (is_resizing_right) { 1298 | if (parent_bar_id === bar.task.id) { 1299 | bar.update_bar_position({ 1300 | width: $bar.owidth + $bar.finaldx, 1301 | }); 1302 | } 1303 | } else if ( 1304 | is_dragging && 1305 | !this.options.readonly && 1306 | !this.options.readonly_dates 1307 | ) { 1308 | bar.update_bar_position({ x: $bar.ox + $bar.finaldx }); 1309 | } 1310 | }); 1311 | }); 1312 | 1313 | document.addEventListener('mouseup', () => { 1314 | is_dragging = false; 1315 | is_resizing_left = false; 1316 | is_resizing_right = false; 1317 | this.$container 1318 | .querySelector('.visible') 1319 | ?.classList?.remove?.('visible'); 1320 | }); 1321 | 1322 | $.on(this.$svg, 'mouseup', (e) => { 1323 | this.bar_being_dragged = null; 1324 | bars.forEach((bar) => { 1325 | const $bar = bar.$bar; 1326 | if (!$bar.finaldx) return; 1327 | bar.date_changed(); 1328 | bar.compute_progress(); 1329 | bar.set_action_completed(); 1330 | }); 1331 | }); 1332 | 1333 | this.bind_bar_progress(); 1334 | } 1335 | 1336 | bind_bar_progress() { 1337 | let x_on_start = 0; 1338 | let is_resizing = null; 1339 | let bar = null; 1340 | let $bar_progress = null; 1341 | let $bar = null; 1342 | 1343 | $.on(this.$svg, 'mousedown', '.handle.progress', (e, handle) => { 1344 | is_resizing = true; 1345 | x_on_start = e.offsetX || e.layerX; 1346 | y_on_start = e.offsetY || e.layerY; 1347 | 1348 | const $bar_wrapper = $.closest('.bar-wrapper', handle); 1349 | const id = $bar_wrapper.getAttribute('data-id'); 1350 | bar = this.get_bar(id); 1351 | 1352 | $bar_progress = bar.$bar_progress; 1353 | $bar = bar.$bar; 1354 | 1355 | $bar_progress.finaldx = 0; 1356 | $bar_progress.owidth = $bar_progress.getWidth(); 1357 | $bar_progress.min_dx = -$bar_progress.owidth; 1358 | $bar_progress.max_dx = $bar.getWidth() - $bar_progress.getWidth(); 1359 | }); 1360 | 1361 | const range_positions = this.config.ignored_positions.map((d) => [ 1362 | d, 1363 | d + this.config.column_width, 1364 | ]); 1365 | 1366 | $.on(this.$svg, 'mousemove', (e) => { 1367 | if (!is_resizing) return; 1368 | let now_x = e.offsetX || e.layerX; 1369 | 1370 | let moving_right = now_x > x_on_start; 1371 | if (moving_right) { 1372 | let k = range_positions.find( 1373 | ([begin, end]) => now_x >= begin && now_x < end, 1374 | ); 1375 | while (k) { 1376 | now_x = k[1]; 1377 | k = range_positions.find( 1378 | ([begin, end]) => now_x >= begin && now_x < end, 1379 | ); 1380 | } 1381 | } else { 1382 | let k = range_positions.find( 1383 | ([begin, end]) => now_x > begin && now_x <= end, 1384 | ); 1385 | while (k) { 1386 | now_x = k[0]; 1387 | k = range_positions.find( 1388 | ([begin, end]) => now_x > begin && now_x <= end, 1389 | ); 1390 | } 1391 | } 1392 | 1393 | let dx = now_x - x_on_start; 1394 | if (dx > $bar_progress.max_dx) { 1395 | dx = $bar_progress.max_dx; 1396 | } 1397 | if (dx < $bar_progress.min_dx) { 1398 | dx = $bar_progress.min_dx; 1399 | } 1400 | 1401 | $bar_progress.setAttribute('width', $bar_progress.owidth + dx); 1402 | $.attr(bar.$handle_progress, 'cx', $bar_progress.getEndX()); 1403 | 1404 | $bar_progress.finaldx = dx; 1405 | }); 1406 | 1407 | $.on(this.$svg, 'mouseup', () => { 1408 | is_resizing = false; 1409 | if (!($bar_progress && $bar_progress.finaldx)) return; 1410 | 1411 | $bar_progress.finaldx = 0; 1412 | bar.progress_changed(); 1413 | bar.set_action_completed(); 1414 | bar = null; 1415 | $bar_progress = null; 1416 | $bar = null; 1417 | }); 1418 | } 1419 | 1420 | get_all_dependent_tasks(task_id) { 1421 | let out = []; 1422 | let to_process = [task_id]; 1423 | while (to_process.length) { 1424 | const deps = to_process.reduce((acc, curr) => { 1425 | acc = acc.concat(this.dependency_map[curr]); 1426 | return acc; 1427 | }, []); 1428 | 1429 | out = out.concat(deps); 1430 | to_process = deps.filter((d) => !to_process.includes(d)); 1431 | } 1432 | 1433 | return out.filter(Boolean); 1434 | } 1435 | 1436 | get_snap_position(dx, ox) { 1437 | let unit_length = 1; 1438 | const default_snap = 1439 | this.options.snap_at || this.config.view_mode.snap_at || '1d'; 1440 | 1441 | if (default_snap !== 'unit') { 1442 | const { duration, scale } = date_utils.parse_duration(default_snap); 1443 | unit_length = 1444 | date_utils.convert_scales(this.config.view_mode.step, scale) / 1445 | duration; 1446 | } 1447 | 1448 | const rem = dx % (this.config.column_width / unit_length); 1449 | 1450 | let final_dx = 1451 | dx - 1452 | rem + 1453 | (rem < (this.config.column_width / unit_length) * 2 1454 | ? 0 1455 | : this.config.column_width / unit_length); 1456 | let final_pos = ox + final_dx; 1457 | 1458 | const drn = final_dx > 0 ? 1 : -1; 1459 | let ignored_regions = this.get_ignored_region(final_pos, drn); 1460 | while (ignored_regions.length) { 1461 | final_pos += this.config.column_width * drn; 1462 | ignored_regions = this.get_ignored_region(final_pos, drn); 1463 | if (!ignored_regions.length) 1464 | final_pos -= this.config.column_width * drn; 1465 | } 1466 | return final_pos - ox; 1467 | } 1468 | 1469 | get_ignored_region(pos, drn = 1) { 1470 | if (drn === 1) { 1471 | return this.config.ignored_positions.filter((val) => { 1472 | return pos > val && pos <= val + this.config.column_width; 1473 | }); 1474 | } else { 1475 | return this.config.ignored_positions.filter( 1476 | (val) => pos >= val && pos < val + this.config.column_width, 1477 | ); 1478 | } 1479 | } 1480 | 1481 | unselect_all() { 1482 | if (this.popup) this.popup.parent.classList.add('hide'); 1483 | this.$container 1484 | .querySelectorAll('.date-range-highlight') 1485 | .forEach((k) => k.classList.add('hide')); 1486 | } 1487 | 1488 | view_is(modes) { 1489 | if (typeof modes === 'string') { 1490 | return this.config.view_mode.name === modes; 1491 | } 1492 | 1493 | if (Array.isArray(modes)) { 1494 | return modes.some(view_is); 1495 | } 1496 | 1497 | return this.config.view_mode.name === modes.name; 1498 | } 1499 | 1500 | get_task(id) { 1501 | return this.tasks.find((task) => { 1502 | return task.id === id; 1503 | }); 1504 | } 1505 | 1506 | get_bar(id) { 1507 | return this.bars.find((bar) => { 1508 | return bar.task.id === id; 1509 | }); 1510 | } 1511 | 1512 | show_popup(opts) { 1513 | if (this.options.popup === false) return; 1514 | if (!this.popup) { 1515 | this.popup = new Popup( 1516 | this.$popup_wrapper, 1517 | this.options.popup, 1518 | this, 1519 | ); 1520 | } 1521 | this.popup.show(opts); 1522 | } 1523 | 1524 | hide_popup() { 1525 | this.popup && this.popup.hide(); 1526 | } 1527 | 1528 | trigger_event(event, args) { 1529 | if (this.options['on' + event]) { 1530 | this.options['on' + event].apply(this, args); 1531 | } 1532 | } 1533 | 1534 | /** 1535 | * Gets the oldest starting date from the list of tasks 1536 | * 1537 | * @returns Date 1538 | * @memberof Gantt 1539 | / 1540 | get_oldest_starting_date() { 1541 | if (!this.tasks.length) return new Date(); 1542 | return this.tasks 1543 | .map((task) => task._start) 1544 | .reduce((prev_date, cur_date) => 1545 | cur_date <= prev_date ? cur_date : prev_date, 1546 | ); 1547 | } 1548 | 1549 | /* 1550 | * Clear all elements from the parent svg element 1551 | * 1552 | * @memberof Gantt 1553 | */ 1554 | clear() { 1555 | this.$svg.innerHTML = ''; 1556 | this.$header?.remove?.(); 1557 | this.$side_header?.remove?.(); 1558 | this.$current_highlight?.remove?.(); 1559 | this.$extras?.remove?.(); 1560 | this.popup?.hide?.(); 1561 | } 1562 | } 1563 | 1564 | Gantt.VIEW_MODE = { 1565 | HOUR: DEFAULT_VIEW_MODES[0], 1566 | QUARTER_DAY: DEFAULT_VIEW_MODES[1], 1567 | HALF_DAY: DEFAULT_VIEW_MODES[2], 1568 | DAY: DEFAULT_VIEW_MODES[3], 1569 | WEEK: DEFAULT_VIEW_MODES[4], 1570 | MONTH: DEFAULT_VIEW_MODES[5], 1571 | YEAR: DEFAULT_VIEW_MODES[6], 1572 | }; 1573 | 1574 | function generate_id(task) { 1575 | return task.name + '' + Math.random().toString(36).slice(2, 12); 1576 | } 1577 | 1578 | function sanitize(s) { 1579 | return s.replaceAll(' ', '').replaceAll(':', '').replaceAll('.', ''); 1580 | } 1581 |


/src/popup.js:

1 | export default class Popup { 2 | constructor(parent, popup_func, gantt) { 3 | this.parent = parent; 4 | this.popup_func = popup_func; 5 | this.gantt = gantt; 6 | 7 | this.make(); 8 | } 9 | 10 | make() { 11 | this.parent.innerHTML = 12 | <div class="title"></div> 13 | <div class="subtitle"></div> 14 | <div class="details"></div> 15 | <div class="actions"></div> 16 | ; 17 | this.hide(); 18 | 19 | this.title = this.parent.querySelector('.title'); 20 | this.subtitle = this.parent.querySelector('.subtitle'); 21 | this.details = this.parent.querySelector('.details'); 22 | this.actions = this.parent.querySelector('.actions'); 23 | } 24 | 25 | show({ x, y, task, target }) { 26 | this.actions.innerHTML = ''; 27 | let html = this.popup_func({ 28 | task, 29 | chart: this.gantt, 30 | get_title: () => this.title, 31 | set_title: (title) => (this.title.innerHTML = title), 32 | get_subtitle: () => this.subtitle, 33 | set_subtitle: (subtitle) => (this.subtitle.innerHTML = subtitle), 34 | get_details: () => this.details, 35 | set_details: (details) => (this.details.innerHTML = details), 36 | add_action: (html, func) => { 37 | let action = this.gantt.create_el({ 38 | classes: 'action-btn', 39 | type: 'button', 40 | append_to: this.actions, 41 | }); 42 | if (typeof html === 'function') html = html(task); 43 | action.innerHTML = html; 44 | action.onclick = (e) => func(task, this.gantt, e); 45 | }, 46 | }); 47 | if (html === false) return; 48 | if (html) this.parent.innerHTML = html; 49 | 50 | if (this.actions.innerHTML === '') this.actions.remove(); 51 | else this.parent.appendChild(this.actions); 52 | 53 | this.parent.style.left = x + 10 + 'px'; 54 | this.parent.style.top = y - 10 + 'px'; 55 | this.parent.classList.remove('hide'); 56 | } 57 | 58 | hide() { 59 | this.parent.classList.add('hide'); 60 | } 61 | } 62 |


/src/styles/dark.css:

1 | :root { 2 | --g-bar-stroke-dark: #c6ccd2; 3 | --g-border-color-dark: #616161; 4 | --g-bar-color-dark: #616161; 5 | --g-bg-dark: #3e3e3e; 6 | --g-light-border-color-dark: #3e3e3e; 7 | --g-text-muted-dark: #eee; 8 | --g-text-light-dark: #ececec; 9 | --g-text-color-dark: #f7f7f7; 10 | --g-progress-color: #8a8aff; 11 | } 12 | 13 | .dark > .gantt-container .gantt { 14 | & .grid-row { 15 | fill: #252525; 16 | } 17 | 18 | & .row-line { 19 | stroke: var(--g-light-border-color-dark); 20 | } 21 | 22 | & .tick { 23 | stroke: var(--g-border-color-dark); 24 | } 25 | 26 | & .arrow { 27 | stroke: var(--g-text-muted-dark); 28 | } 29 | 30 | & .bar { 31 | fill: var(--g-bar-color-dark); 32 | stroke: none; 33 | } 34 | 35 | & .bar-progress { 36 | fill: var(--g-progress-color); 37 | } 38 | 39 | & .bar-invalid { 40 | fill: transparent; 41 | stroke: var(--g-bar-stroke-dark); 42 | 43 | & ~ .bar-label { 44 | fill: var(--g-text-light-dark); 45 | } 46 | } 47 | 48 | & .bar-label.big { 49 | fill: var(--g-text-light-dark); 50 | } 51 | 52 | & .bar-wrapper { 53 | &:hover { 54 | .bar { 55 | fill: lighten(var(--g-bar-color-dark, 5)); 56 | } 57 | 58 | & .bar-progress { 59 | fill: lighten(var(--g-progress-color, 5)); 60 | } 61 | } 62 | 63 | &.active { 64 | .bar { 65 | fill: lighten(var(--g-bar-color-dark, 5)); 66 | } 67 | 68 | & .bar-progress { 69 | fill: lighten(var(--g-progress-color, 5)); 70 | } 71 | } 72 | } 73 | } 74 | 75 | .dark > .gantt-container { 76 | & .grid-header { 77 | background-color: #252525; 78 | } 79 | 80 | & .popup-wrapper { 81 | background-color: #333; 82 | 83 | & .title { 84 | border-color: lighten(var(--g-progress-color, 5)); 85 | } 86 | } 87 | } 88 |


/src/styles/gantt.css:

1 | @import './light.css'; 2 | 3 | .gantt-container { 4 | line-height: 14.5px; 5 | position: relative; 6 | overflow: auto; 7 | font-size: 12px; 8 | height: var(--gv-grid-height); 9 | width: 100%; 10 | border-radius: 8px; 11 | 12 | & .popup-wrapper { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | background: #fff; 17 | box-shadow: 0px 10px 24px -3px rgba(0, 0, 0, 0.2); 18 | padding: 10px; 19 | border-radius: 5px; 20 | width: max-content; 21 | z-index: 1000; 22 | 23 | & .title { 24 | margin-bottom: 2px; 25 | color: var(--g-text-dark); 26 | font-size: 0.85rem; 27 | font-weight: 650; 28 | line-height: 15px; 29 | } 30 | 31 | & .subtitle { 32 | color: var(--g-text-dark); 33 | font-size: 0.8rem; 34 | margin-bottom: 5px; 35 | } 36 | 37 | & .details { 38 | color: var(--g-text-muted); 39 | font-size: 0.7rem; 40 | } 41 | 42 | & .actions { 43 | margin-top: 10px; 44 | margin-left: 3px; 45 | } 46 | 47 | & .action-btn { 48 | border: none; 49 | padding: 5px 8px; 50 | background-color: var(--g-popup-actions); 51 | border-right: 1px solid var(--g-text-light); 52 | 53 | &:hover { 54 | background-color: brightness(97%); 55 | } 56 | 57 | &:first-child { 58 | border-top-left-radius: 4px; 59 | border-bottom-left-radius: 4px; 60 | } 61 | 62 | &:last-child { 63 | border-right: none; 64 | border-top-right-radius: 4px; 65 | border-bottom-right-radius: 4px; 66 | } 67 | } 68 | } 69 | 70 | & .grid-header { 71 | height: calc( 72 | var(--gv-lower-header-height) + var(--gv-upper-header-height) + 10px 73 | ); 74 | background-color: var(--g-header-background); 75 | position: sticky; 76 | top: 0; 77 | left: 0; 78 | border-bottom: 1px solid var(--g-row-border-color); 79 | z-index: 1000; 80 | } 81 | 82 | & .lower-text, 83 | & .upper-text { 84 | text-anchor: middle; 85 | } 86 | 87 | & .upper-header { 88 | height: var(--gv-upper-header-height); 89 | } 90 | 91 | & .lower-header { 92 | height: var(--gv-lower-header-height); 93 | } 94 | 95 | & .lower-text { 96 | font-size: 12px; 97 | position: absolute; 98 | width: calc(var(--gv-column-width) * 0.8); 99 | height: calc(var(--gv-lower-header-height) * 0.8); 100 | margin: 0 calc(var(--gv-column-width) * 0.1); 101 | align-content: center; 102 | text-align: center; 103 | color: var(--g-text-muted); 104 | } 105 | 106 | & .upper-text { 107 | position: absolute; 108 | width: fit-content; 109 | font-weight: 500; 110 | font-size: 14px; 111 | color: var(--g-text-dark); 112 | height: calc(var(--gv-lower-header-height) * 0.66); 113 | } 114 | 115 | & .current-upper { 116 | position: sticky; 117 | left: 0 !important; 118 | padding-left: 17px; 119 | background: white; 120 | } 121 | 122 | & .side-header { 123 | position: sticky; 124 | top: 0; 125 | right: 0; 126 | float: right; 127 | 128 | z-index: 1000; 129 | line-height: 20px; 130 | font-weight: 400; 131 | width: max-content; 132 | margin-left: auto; 133 | padding-right: 10px; 134 | padding-top: 10px; 135 | background: var(--g-header-background); 136 | display: flex; 137 | } 138 | 139 | & .side-header * { 140 | transition-property: background-color; 141 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 142 | transition-duration: 150ms; 143 | background-color: var(--g-actions-background); 144 | border-radius: 0.5rem; 145 | border: none; 146 | padding: 5px 8px; 147 | color: var(--g-text-dark); 148 | font-size: 14px; 149 | letter-spacing: 0.02em; 150 | font-weight: 420; 151 | box-sizing: content-box; 152 | 153 | margin-right: 5px; 154 | 155 | &:last-child { 156 | margin-right: 0; 157 | } 158 | 159 | &:hover { 160 | filter: brightness(97.5%); 161 | } 162 | } 163 | 164 | & .side-header select { 165 | width: 60px; 166 | padding-top: 2px; 167 | padding-bottom: 2px; 168 | } 169 | & .side-header select:focus { 170 | outline: none; 171 | } 172 | 173 | & .date-range-highlight { 174 | background-color: var(--g-progress-color); 175 | border-radius: 12px; 176 | height: calc(var(--gv-lower-header-height) - 6px); 177 | top: calc(var(--gv-upper-header-height) + 5px); 178 | position: absolute; 179 | } 180 | 181 | & .current-highlight { 182 | position: absolute; 183 | background: var(--g-today-highlight); 184 | width: 1px; 185 | z-index: 999; 186 | } 187 | 188 | & .current-ball-highlight { 189 | position: absolute; 190 | background: var(--g-today-highlight); 191 | z-index: 1001; 192 | border-radius: 50%; 193 | } 194 | 195 | & .current-date-highlight { 196 | background: var(--g-today-highlight); 197 | color: var(--g-text-light); 198 | border-radius: 5px; 199 | } 200 | 201 | & .holiday-label { 202 | position: absolute; 203 | top: 0; 204 | left: 0; 205 | opacity: 0; 206 | z-index: 1000; 207 | background: --g-weekend-label-color; 208 | border-radius: 5px; 209 | padding: 2px 5px; 210 | 211 | &.show { 212 | opacity: 100; 213 | } 214 | } 215 | 216 | & .extras { 217 | position: sticky; 218 | left: 0px; 219 | 220 | & .adjust { 221 | position: absolute; 222 | left: 8px; 223 | top: calc(var(--gv-grid-height) - 60px); 224 | background-color: rgba(0, 0, 0, 0.7); 225 | color: white; 226 | border: none; 227 | padding: 8px; 228 | border-radius: 3px; 229 | } 230 | } 231 | 232 | .hide { 233 | display: none; 234 | } 235 | } 236 | 237 | .gantt { 238 | user-select: none; 239 | -webkit-user-select: none; 240 | position: absolute; 241 | 242 | & .grid-background { 243 | fill: none; 244 | } 245 | 246 | & .grid-row { 247 | fill: var(--g-row-color); 248 | } 249 | 250 | & .row-line { 251 | stroke: var(--g-border-color); 252 | } 253 | 254 | & .tick { 255 | stroke: var(--g-tick-color); 256 | stroke-width: 0.4; 257 | 258 | &.thick { 259 | stroke: var(--g-tick-color-thick); 260 | stroke-width: 0.7; 261 | } 262 | } 263 | 264 | & .arrow { 265 | fill: none; 266 | stroke: var(--g-arrow-color); 267 | stroke-width: 1.5; 268 | } 269 | 270 | & .bar-wrapper .bar { 271 | fill: var(--g-bar-color); 272 | stroke: var(--g-bar-border); 273 | stroke-width: 0; 274 | transition: stroke-width 0.3s ease; 275 | } 276 | 277 | & .bar-progress { 278 | fill: var(--g-progress-color); 279 | border-radius: 4px; 280 | } 281 | 282 | & .bar-expected-progress { 283 | fill: var(--g-expected-progress); 284 | } 285 | 286 | & .bar-invalid { 287 | fill: transparent; 288 | stroke: var(--g-bar-border); 289 | stroke-width: 1; 290 | stroke-dasharray: 5; 291 | 292 | & ~ .bar-label { 293 | fill: var(--g-text-light); 294 | } 295 | } 296 | 297 | & .bar-label { 298 | fill: var(--g-text-dark); 299 | dominant-baseline: central; 300 | font-family: Helvetica; 301 | font-size: 13px; 302 | font-weight: 400; 303 | 304 | &.big { 305 | fill: var(--g-text-dark); 306 | text-anchor: start; 307 | } 308 | } 309 | 310 | & .handle { 311 | fill: var(--g-handle-color); 312 | opacity: 0; 313 | transition: opacity 0.3s ease; 314 | &.active, 315 | &.visible { 316 | cursor: ew-resize; 317 | opacity: 1; 318 | } 319 | } 320 | 321 | & .handle.progress { 322 | fill: var(--g-text-muted); 323 | } 324 | 325 | & .bar-wrapper { 326 | cursor: pointer; 327 | 328 | & .bar { 329 | outline: 1px solid var(--g-row-border-color); 330 | border-radius: 3px; 331 | } 332 | 333 | &:hover { 334 | .bar { 335 | transition: transform 0.3s ease; 336 | } 337 | 338 | .date-range-highlight { 339 | display: block; 340 | } 341 | } 342 | } 343 | } 344 |


/src/styles/light.css:

1 | :root { 2 | --g-arrow-color: #1f2937; 3 | --g-bar-color: #fff; 4 | --g-bar-border: #fff; 5 | --g-tick-color-thick: #ededed; 6 | --g-tick-color: #f3f3f3; 7 | --g-actions-background: #f3f3f3; 8 | --g-border-color: #ebeff2; 9 | --g-text-muted: #7c7c7c; 10 | --g-text-light: #fff; 11 | --g-text-dark: #171717; 12 | --g-progress-color: #dbdbdb; 13 | --g-handle-color: #37352f; 14 | --g-weekend-label-color: #dcdce4; 15 | --g-expected-progress: #c4c4e9; 16 | --g-header-background: #fff; 17 | --g-row-color: #fdfdfd; 18 | --g-row-border-color: #c7c7c7; 19 | --g-today-highlight: #37352f; 20 | --g-popup-actions: #ebeff2; 21 | --g-weekend-highlight-color: #f7f7f7; 22 | } 23 |


/src/svg_utils.js:

1 | export function $(expr, con) { 2 | return typeof expr === 'string' 3 | ? (con || document).querySelector(expr) 4 | : expr || null; 5 | } 6 | 7 | export function createSVG(tag, attrs) { 8 | const elem = document.createElementNS('http://www.w3.org/2000/svg', tag); 9 | for (let attr in attrs) { 10 | if (attr === 'append_to') { 11 | const parent = attrs.append_to; 12 | parent.appendChild(elem); 13 | } else if (attr === 'innerHTML') { 14 | elem.innerHTML = attrs.innerHTML; 15 | } else if (attr === 'clipPath') { 16 | elem.setAttribute('clip-path', 'url(#' + attrs[attr] + ')'); 17 | } else { 18 | elem.setAttribute(attr, attrs[attr]); 19 | } 20 | } 21 | return elem; 22 | } 23 | 24 | export function animateSVG(svgElement, attr, from, to) { 25 | const animatedSvgElement = getAnimationElement(svgElement, attr, from, to); 26 | 27 | if (animatedSvgElement === svgElement) { 28 | // triggered 2nd time programmatically 29 | // trigger artificial click event 30 | const event = document.createEvent('HTMLEvents'); 31 | event.initEvent('click', true, true); 32 | event.eventName = 'click'; 33 | animatedSvgElement.dispatchEvent(event); 34 | } 35 | } 36 | 37 | function getAnimationElement( 38 | svgElement, 39 | attr, 40 | from, 41 | to, 42 | dur = '0.4s', 43 | begin = '0.1s', 44 | ) { 45 | const animEl = svgElement.querySelector('animate'); 46 | if (animEl) { 47 | $.attr(animEl, { 48 | attributeName: attr, 49 | from, 50 | to, 51 | dur, 52 | begin: 'click + ' + begin, // artificial click 53 | }); 54 | return svgElement; 55 | } 56 | 57 | const animateElement = createSVG('animate', { 58 | attributeName: attr, 59 | from, 60 | to, 61 | dur, 62 | begin, 63 | calcMode: 'spline', 64 | values: from + ';' + to, 65 | keyTimes: '0; 1', 66 | keySplines: cubic_bezier('ease-out'), 67 | }); 68 | svgElement.appendChild(animateElement); 69 | 70 | return svgElement; 71 | } 72 | 73 | function cubic_bezier(name) { 74 | return { 75 | ease: '.25 .1 .25 1', 76 | linear: '0 0 1 1', 77 | 'ease-in': '.42 0 1 1', 78 | 'ease-out': '0 0 .58 1', 79 | 'ease-in-out': '.42 0 .58 1', 80 | }[name]; 81 | } 82 | 83 | $.on = (element, event, selector, callback) => { 84 | if (!callback) { 85 | callback = selector; 86 | $.bind(element, event, callback); 87 | } else { 88 | $.delegate(element, event, selector, callback); 89 | } 90 | }; 91 | 92 | $.off = (element, event, handler) =&gt; { 93 | element.removeEventListener(event, handler); 94 | }; 95 | 96 | $.bind = (element, event, callback) => { 97 | event.split(/\s+/).forEach(function (event) { 98 | element.addEventListener(event, callback); 99 | }); 100 | }; 101 | 102 | $.delegate = (element, event, selector, callback) => { 103 | element.addEventListener(event, function (e) { 104 | const delegatedTarget = e.target.closest(selector); 105 | if (delegatedTarget) { 106 | e.delegatedTarget = delegatedTarget; 107 | callback.call(this, e, delegatedTarget); 108 | } 109 | }); 110 | }; 111 | 112 | $.closest = (selector, element) => { 113 | if (!element) return null; 114 | 115 | if (element.matches(selector)) { 116 | return element; 117 | } 118 | 119 | return $.closest(selector, element.parentNode); 120 | }; 121 | 122 | $.attr = (element, attr, value) => { 123 | if (!value && typeof attr === 'string') { 124 | return element.getAttribute(attr); 125 | } 126 | 127 | if (typeof attr === 'object') { 128 | for (let key in attr) { 129 | $.attr(element, key, attr[key]); 130 | } 131 | return; 132 | } 133 | 134 | element.setAttribute(attr, value); 135 | }; 136 |


/tests/date_utils.test.js:

1 | import date_utils from '../src/date_utils'; 2 | 3 | test('Parse: parses string date', () => { 4 | const date = date_utils.parse('2017-09-09'); 5 | 6 | expect(date.getDate()).toBe(9); 7 | expect(date.getMonth()).toBe(8); 8 | expect(date.getFullYear()).toBe(2017); 9 | }); 10 | 11 | test('Parse: parses string datetime', () => { 12 | const date = date_utils.parse('2017-08-27 16:08:34'); 13 | 14 | expect(date.getFullYear()).toBe(2017); 15 | expect(date.getMonth()).toBe(7); 16 | expect(date.getDate()).toBe(27); 17 | expect(date.getHours()).toBe(16); 18 | expect(date.getMinutes()).toBe(8); 19 | expect(date.getSeconds()).toBe(34); 20 | }); 21 | 22 | test('Parse: parses string datetime', () => { 23 | const date = date_utils.parse('2016-02-29 16:08:34.3'); 24 | 25 | expect(date.getFullYear()).toBe(2016); 26 | expect(date.getMonth()).toBe(1); 27 | expect(date.getDate()).toBe(29); 28 | expect(date.getHours()).toBe(16); 29 | expect(date.getMinutes()).toBe(8); 30 | expect(date.getSeconds()).toBe(34); 31 | expect(date.getMilliseconds()).toBe(300); 32 | }); 33 | 34 | test('Parse: parses string datetime', () => { 35 | const date = date_utils.parse('2015-07-01 00:00:59.200'); 36 | 37 | expect(date.getFullYear()).toBe(2015); 38 | expect(date.getMonth()).toBe(6); 39 | expect(date.getDate()).toBe(1); 40 | expect(date.getHours()).toBe(0); 41 | expect(date.getMinutes()).toBe(0); 42 | expect(date.getSeconds()).toBe(59); 43 | expect(date.getMilliseconds()).toBe(200); 44 | }); 45 | 46 | test('Format: converts date object to string', () => { 47 | const date = new Date('2017-09-18'); 48 | expect(date_utils.to_string(date)).toBe('2017-09-18'); 49 | }); 50 | 51 | test('Format: converts date object to string', () => { 52 | const date = new Date('2016-02-29 16:08:34.3'); 53 | expect(date_utils.to_string(date, true)).toBe('2016-02-29 16:08:34.300'); 54 | }); 55 | 56 | test('Format: converts date object to string', () => { 57 | const date = new Date('2016-02-29 16:08:34.3'); 58 | expect(date_utils.to_string(date, true)).toBe('2016-02-29 16:08:34.300'); 59 | }); 60 | 61 | test('Parse: returns Date Object as is', () => { 62 | const d = new Date(); 63 | const date = date_utils.parse(d); 64 | 65 | expect(d).toBe(date); 66 | }); 67 | 68 | test('Diff: returns diff between 2 date objects', () => { 69 | const a = date_utils.parse('2017-09-08'); 70 | const b = date_utils.parse('2017-06-07'); 71 | 72 | expect(date_utils.diff(a, b, 'day')).toBe(93); 73 | expect(date_utils.diff(a, b, 'month')).toBe(3); 74 | expect(date_utils.diff(a, b, 'year')).toBe(0); 75 | }); 76 | 77 | test('StartOf', () => { 78 | const date = date_utils.parse('2017-08-12 15:07:34.012'); 79 | 80 | const start_of_millisecond = date_utils.start_of(date, 'millisecond'); 81 | expect(date_utils.to_string(start_of_millisecond, true)).toBe( 82 | '2017-08-12 15:07:34.012', 83 | ); 84 | 85 | const start_of_second = date_utils.start_of(date, 'second'); 86 | expect(date_utils.to_string(start_of_second, true)).toBe( 87 | '2017-08-12 15:07:34.000', 88 | ); 89 | 90 | const start_of_minute = date_utils.start_of(date, 'minute'); 91 | expect(date_utils.to_string(start_of_minute, true)).toBe( 92 | '2017-08-12 15:07:00.000', 93 | ); 94 | 95 | const start_of_hour = date_utils.start_of(date, 'hour'); 96 | expect(date_utils.to_string(start_of_hour, true)).toBe( 97 | '2017-08-12 15:00:00.000', 98 | ); 99 | 100 | const start_of_day = date_utils.start_of(date, 'day'); 101 | expect(date_utils.to_string(start_of_day, true)).toBe( 102 | '2017-08-12 00:00:00.000', 103 | ); 104 | 105 | const start_of_month = date_utils.start_of(date, 'month'); 106 | expect(date_utils.to_string(start_of_month, true)).toBe( 107 | '2017-08-01 00:00:00.000', 108 | ); 109 | 110 | const start_of_year = date_utils.start_of(date, 'year'); 111 | expect(date_utils.to_string(start_of_year, true)).toBe( 112 | '2017-01-01 00:00:00.000', 113 | ); 114 | }); 115 | 116 | test('format', () => { 117 | const date = date_utils.parse('2017-08-12 15:07:23'); 118 | expect(date_utils.format(date, 'YYYY-MM-DD')).toBe('2017-08-12'); 119 | }); 120 | 121 | test('format', () => { 122 | const date = date_utils.parse('2016-02-29 16:08:34.3'); 123 | expect(date_utils.format(date)).toBe('2016-02-29 16:08:34.300'); 124 | }); 125 |


/vite.config.js:

1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: resolve(__dirname, 'src/index.js'), 8 | name: 'Gantt', 9 | fileName: 'frappe-gantt', 10 | }, 11 | rollupOptions: { 12 | output: { 13 | format: 'cjs', 14 | assetFileNames: 'frappe-gantt[extname]', 15 | entryFileNames: 'frappe-gantt.[format].js' 16 | }, 17 | }, 18 | }, 19 | output: { interop: 'auto' }, 20 | server: { watch: { include: ['dist/', 'src/'] } } 21 | });


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment