├── .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
https://raw.githubusercontent.com/frappe/gantt/108eeb5898043eb870a47617a878bb253237c324/.github/gantt-logo.jpg
https://raw.githubusercontent.com/frappe/gantt/108eeb5898043eb870a47617a878bb253237c324/.github/hero-image.png
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 |
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 |
1 | dist
1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true 4 | } 5 |
1 |
7 | 8 | 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 |
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 |
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 |
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 | }];
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 |
40 |
44 | Easy make sure your employees change only what 45 | they need to. 46 |
47 |92 | Change the view mode, or scroll to today, or add 93 | anything you like β. 94 |
95 |126 | Be it public holidays, company milestones, or just 127 | weekends, you can see it all. 128 |
129 |148 | Remove time periods from your Gantt - they're now 149 | completely ignored. 150 |
151 |283 | Insane levels of customizability - change anything, 284 | everything. 285 |
286 |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.
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 |
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 |
1 | /* eslint-disable */ 2 | module.exports = { 3 | plugins: [require('postcss-nesting')], 4 | };
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 |
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 | .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | });