adore-gantt 2.0.0

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