tg-ganttchart 0.0.7 → 0.0.8

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.
Files changed (66) hide show
  1. package/babel.config.js +5 -0
  2. package/package.json +1 -4
  3. package/src/.eslintrc.js +18 -0
  4. package/src/App.vue +780 -0
  5. package/src/GanttElastic.standalone.vue +48 -0
  6. package/src/GanttElastic.vue +2305 -0
  7. package/src/assets/logo.png +0 -0
  8. package/src/components/Calendar/Calendar.vue +559 -0
  9. package/src/components/Calendar/CalendarRow.vue +112 -0
  10. package/src/components/Chart/Chart.vue +117 -0
  11. package/src/components/Chart/DaysHighlight.vue +60 -0
  12. package/src/components/Chart/DependencyLines.vue +112 -0
  13. package/src/components/Chart/Grid.vue +205 -0
  14. package/src/components/Chart/ProgressBar.vue +110 -0
  15. package/src/components/Chart/Row/Epic.vue +131 -0
  16. package/src/components/Chart/Row/Milestone.vue +117 -0
  17. package/src/components/Chart/Row/Project.vue +132 -0
  18. package/src/components/Chart/Row/Story.vue +127 -0
  19. package/src/components/Chart/Row/Subtask.vue +117 -0
  20. package/src/components/Chart/Row/Task.mixin.js +47 -0
  21. package/src/components/Chart/Row/Task.vue +82 -0
  22. package/src/components/Chart/Text.vue +105 -0
  23. package/src/components/Expander.vue +114 -0
  24. package/src/components/GanttElastic.standalone.vue +48 -0
  25. package/src/components/GanttElastic.vue +1646 -0
  26. package/src/components/Header/GanttViewFilter.vue +154 -0
  27. package/src/components/Header/Header.vue +266 -0
  28. package/src/components/MainView.vue +283 -0
  29. package/src/components/TaskList/ItemColumn.vue +212 -0
  30. package/src/components/TaskList/TaskList.vue +45 -0
  31. package/src/components/TaskList/TaskListHeader.vue +143 -0
  32. package/src/components/TaskList/TaskListItem.vue +35 -0
  33. package/src/components/bundle.js +28 -0
  34. package/src/components/components/Calendar/Calendar.vue +332 -0
  35. package/src/components/components/Calendar/CalendarRow.vue +96 -0
  36. package/src/components/components/Chart/Chart.vue +111 -0
  37. package/src/components/components/Chart/DaysHighlight.vue +71 -0
  38. package/src/components/components/Chart/DependencyLines.vue +112 -0
  39. package/src/components/components/Chart/Grid.vue +164 -0
  40. package/src/components/components/Chart/ProgressBar.vue +110 -0
  41. package/src/components/components/Chart/Row/Milestone.vue +117 -0
  42. package/src/components/components/Chart/Row/Project.vue +131 -0
  43. package/src/components/components/Chart/Row/Task.mixin.js +46 -0
  44. package/src/components/components/Chart/Row/Task.vue +107 -0
  45. package/src/components/components/Chart/Text.vue +105 -0
  46. package/src/components/components/Expander.vue +126 -0
  47. package/src/components/components/Header/Header.vue +265 -0
  48. package/src/components/components/MainView.vue +282 -0
  49. package/src/components/components/TaskList/ItemColumn.vue +121 -0
  50. package/src/components/components/TaskList/TaskList.vue +45 -0
  51. package/src/components/components/TaskList/TaskListHeader.vue +143 -0
  52. package/src/components/components/TaskList/TaskListItem.vue +35 -0
  53. package/src/components/components/bundle.js +28 -0
  54. package/src/components/style.js +308 -0
  55. package/src/index.js +12 -0
  56. package/src/main.js +6 -0
  57. package/src/style.js +398 -0
  58. package/vue.config.js +42 -0
  59. package/dist/demo.html +0 -1
  60. package/dist/tgganttchart.common.js +0 -9232
  61. package/dist/tgganttchart.common.js.map +0 -1
  62. package/dist/tgganttchart.css +0 -1
  63. package/dist/tgganttchart.umd.js +0 -9243
  64. package/dist/tgganttchart.umd.js.map +0 -1
  65. package/dist/tgganttchart.umd.min.js +0 -7
  66. package/dist/tgganttchart.umd.min.js.map +0 -1
@@ -0,0 +1,2305 @@
1
+ <template>
2
+ <div class="gantt-elastic" style="width:100%">
3
+ <gantt-header v-if="isHeaderVisible == true" slot="header"></gantt-header>
4
+ <slot name="header"></slot>
5
+ <main-view ref="mainView"></main-view>
6
+ <slot name="footer"></slot>
7
+ </div>
8
+ </template>
9
+
10
+ <script>
11
+ // import VueInstance from 'vue';
12
+ import dayjs from 'dayjs';
13
+
14
+ import MainView from './components/MainView.vue';
15
+ import getStyle from './style.js';
16
+ import ResizeObserver from 'resize-observer-polyfill';
17
+ import GanttHeader from './components/Header/Header.vue';
18
+
19
+
20
+ const ctx = document.createElement('canvas').getContext('2d');
21
+ // let VueInst = VueInstance;
22
+ // function initVue() {
23
+ // if (typeof Vue !== 'undefined' && typeof VueInst === 'undefined') {
24
+ // VueInst = Vue;
25
+ // }
26
+ // }
27
+ // initVue();
28
+
29
+ let hourWidthCache = null;
30
+
31
+ /**
32
+ * Helper function to fill out empty options in user settings
33
+ *
34
+ * @param {object} userOptions - initial user options that will merge with those below
35
+ * @returns {object} merged options
36
+ */
37
+ function getOptions(userOptions) {
38
+ let localeName = 'en';
39
+ if (typeof userOptions.locale !== 'undefined' && typeof userOptions.locale.name !== 'undefined') {
40
+ localeName = userOptions.locale.name;
41
+ }
42
+ return {
43
+ slots: {
44
+ header: {}
45
+ },
46
+ taskMapping: {
47
+ //*
48
+ id: 'id',
49
+ start: 'start',
50
+ label: 'label',
51
+ duration: 'duration',
52
+ progress: 'progress',
53
+ type: 'type',
54
+ style: 'style',
55
+ collapsed: 'collapsed'
56
+ },
57
+ width: 0,
58
+ height: 0,
59
+ clientWidth: 0,
60
+ outerHeight: 0,
61
+ rowsHeight: 0,
62
+ allVisibleTasksHeight: 0,
63
+ scroll: {
64
+ scrolling: false,
65
+ dragXMoveMultiplier: 3, //*
66
+ dragYMoveMultiplier: 2, //*
67
+ top: 0,
68
+ taskList: {
69
+ left: 0,
70
+ right: 0,
71
+ top: 0,
72
+ bottom: 0
73
+ },
74
+ chart: {
75
+ left: 0,
76
+ right: 0,
77
+ percent: 0,
78
+ timePercent: 0,
79
+ top: 0,
80
+ bottom: 0,
81
+ time: 0,
82
+ timeCenter: 0,
83
+ dateTime: {
84
+ left: '',
85
+ right: ''
86
+ }
87
+ }
88
+ },
89
+ scope: {
90
+ //*
91
+ before: 1,
92
+ after: 1
93
+ },
94
+ times: {
95
+ timeScale: 60 * 1000,
96
+ timeZoom: 17, //*
97
+ timePerPixel: 0,
98
+ firstTime: null,
99
+ lastTime: null,
100
+ firstTaskTime: 0,
101
+ lastTaskTime: 0,
102
+ totalViewDurationMs: 0,
103
+ totalViewDurationPx: 0,
104
+ stepDuration: 'day',
105
+ steps: []
106
+ },
107
+ row: {
108
+ height: 24 //*
109
+ },
110
+ maxRows: 20, //*
111
+ maxHeight: 0, //*
112
+ chart: {
113
+ grid: {
114
+ horizontal: {
115
+ gap: 6 //*
116
+ }
117
+ },
118
+ progress: {
119
+ width: 20, //*
120
+ height: 6, //*
121
+ pattern: true,
122
+ bar: false
123
+ },
124
+ text: {
125
+ offset: 8, //*
126
+ xPadding: 12, //*
127
+ display: true //*
128
+ },
129
+ expander: {
130
+ type: 'chart',
131
+ display: true, //*
132
+ displayIfTaskListHidden: true, //*
133
+ offset: 4, //*
134
+ size: 18
135
+ }
136
+ },
137
+ taskList: {
138
+ display: true, //*
139
+ resizeAfterThreshold: true, //*
140
+ widthThreshold: 75, //*
141
+ columns: [
142
+ //*
143
+ {
144
+ id: 0,
145
+ label: 'ID',
146
+ value: 'id',
147
+ width: 40
148
+ }
149
+ ],
150
+ percent: 100, //*
151
+ width: 0,
152
+ finalWidth: 0,
153
+ widthFromPercentage: 0,
154
+ minWidth: 18,
155
+ expander: {
156
+ type: 'task-list',
157
+ size: 18,
158
+ columnWidth: 24,
159
+ padding: 16,
160
+ margin: 10,
161
+ straight: false
162
+ }
163
+ },
164
+ calendar: {
165
+ workingDays: [1, 2, 3, 4, 5], //*
166
+ gap: 6, //*
167
+ height: 0,
168
+ strokeWidth: 1,
169
+ hour: {
170
+ height: 20, //*
171
+ display: true, //*
172
+ widths: [],
173
+ maxWidths: { short: 0, medium: 0, long: 0 },
174
+ formatted: {
175
+ long: [],
176
+ medium: [],
177
+ short: []
178
+ },
179
+ format: {
180
+ //*
181
+ long(date) {
182
+ return date.format('HH:mm');
183
+ },
184
+ medium(date) {
185
+ return date.format('HH:mm');
186
+ },
187
+ short(date) {
188
+ return date.format('HH');
189
+ }
190
+ }
191
+ },
192
+ day: {
193
+ height: 20, //*
194
+ display: true, //*
195
+ widths: [],
196
+ maxWidths: { short: 0, medium: 0, long: 0 },
197
+ format: {
198
+ long(date) {
199
+ return date.format('DD dddd');
200
+ },
201
+ medium(date) {
202
+ return date.format('DD ddd');
203
+ },
204
+ short(date) {
205
+ return date.format('DD');
206
+ }
207
+ }
208
+ },
209
+ month: {
210
+ height: 20, //*
211
+ display: true, //*
212
+ widths: [],
213
+ maxWidths: { short: 0, medium: 0, long: 0 },
214
+ format: {
215
+ //*
216
+ short(date) {
217
+ return date.format('MM');
218
+ },
219
+ medium(date) {
220
+ return date.format("MMM 'YY");
221
+ },
222
+ long(date) {
223
+ return date.format('MMMM YYYY');
224
+ }
225
+ }
226
+ }
227
+ ,
228
+ quarter: {
229
+ height: 30, //*
230
+ display: true, //*
231
+ widths: [],
232
+ maxWidths: { short: 0, medium: 0, long: 0 },
233
+ format: {
234
+ //*
235
+ short(date) {
236
+ return date.format('Qo quarter');
237
+ },
238
+ medium(date) {
239
+ return date.format("Qo 'YY");
240
+ },
241
+ long(date) {
242
+ return date.format('Qo quarter YYYY');
243
+ }
244
+ }
245
+ }
246
+ },
247
+ locale: {
248
+ //*
249
+ name: 'en',
250
+ weekdays: 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
251
+ weekdaysShort: 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
252
+ weekdaysMin: 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
253
+ months: 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
254
+ monthsShort: 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
255
+ weekStart: 1,
256
+ relativeTime: {
257
+ future: 'in %s',
258
+ past: '%s ago',
259
+ s: 'a few seconds',
260
+ m: 'a minute',
261
+ mm: '%d minutes',
262
+ h: 'an hour',
263
+ hh: '%d hours',
264
+ d: 'a day',
265
+ dd: '%d days',
266
+ M: 'a month',
267
+ MM: '%d months',
268
+ y: 'a year',
269
+ yy: '%d years'
270
+ },
271
+ formats: {
272
+ LT: 'HH:mm',
273
+ LTS: 'HH:mm:ss',
274
+ L: 'DD/MM/YYYY',
275
+ LL: 'D MMMM YYYY',
276
+ LLL: 'D MMMM YYYY HH:mm',
277
+ LLLL: 'dddd, D MMMM YYYY HH:mm'
278
+ },
279
+ ordinal: n => {
280
+ const s = ['th', 'st', 'nd', 'rd'];
281
+ const v = n % 100;
282
+ return `[${n}${s[(v - 20) % 10] || s[v] || s[0]}]`;
283
+ }
284
+ }
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Prepare style
290
+ *
291
+ * @returns {object}
292
+ */
293
+ function prepareStyle(userStyle) {
294
+ let fontSize = '12px';
295
+ let fontFamily = window
296
+ .getComputedStyle(document.body)
297
+ .getPropertyValue('font-family')
298
+ .toString();
299
+ if (typeof userStyle !== 'undefined') {
300
+ if (typeof userStyle.fontSize !== 'undefined') {
301
+ fontSize = userStyle.fontSize;
302
+ }
303
+ if (typeof userStyle.fontFamily !== 'undefined') {
304
+ fontFamily = userStyle.fontFamily;
305
+ }
306
+ }
307
+ return getStyle(fontSize, fontFamily);
308
+ }
309
+
310
+ /**
311
+ * Helper function to determine if specified variable is an object
312
+ *
313
+ * @param {any} item
314
+ *
315
+ * @returns {boolean}
316
+ */
317
+ function isObject(item) {
318
+ return (
319
+ item &&
320
+ typeof item === 'object' &&
321
+ !Array.isArray(item) &&
322
+ !(item instanceof HTMLElement) &&
323
+ !(item instanceof CanvasRenderingContext2D) &&
324
+ typeof item !== 'function'
325
+ );
326
+ }
327
+
328
+ /**
329
+ * Helper function which will merge objects recursively - creating brand new one - like clone
330
+ *
331
+ * @param {object} target
332
+ * @params {object} sources
333
+ *
334
+ * @returns {object}
335
+ */
336
+ export function mergeDeep(target, ...sources) {
337
+ if (!sources.length) {
338
+ return target;
339
+ }
340
+ const source = sources.shift();
341
+ if (isObject(target) && isObject(source)) {
342
+ for (const key in source) {
343
+ if (isObject(source[key])) {
344
+ if (typeof target[key] === 'undefined') {
345
+ target[key] = {};
346
+ }
347
+ target[key] = mergeDeep(target[key], source[key]);
348
+ } else if (Array.isArray(source[key])) {
349
+ target[key] = [];
350
+ for (let item of source[key]) {
351
+ if (isObject(item)) {
352
+ target[key].push(mergeDeep({}, item));
353
+ continue;
354
+ }
355
+ target[key].push(item);
356
+ }
357
+ } else {
358
+ target[key] = source[key];
359
+ }
360
+ }
361
+ }
362
+ return mergeDeep(target, ...sources);
363
+ }
364
+
365
+ /**
366
+ * Detect if object or array is observable
367
+ *
368
+ * @param {object|array} obj
369
+ *
370
+ * @returns {boolean}
371
+ */
372
+ function isObservable(obj) {
373
+ return typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, '__ob__');
374
+ }
375
+
376
+
377
+ /**
378
+ * Same as above but with reactivity in mind
379
+ *
380
+ * @param {object} target
381
+ * @params {object} sources
382
+ *
383
+ * @returns {object}
384
+ */
385
+ export function mergeDeepReactive(component, target, ...sources) {
386
+ if (!sources.length) {
387
+ return target;
388
+ }
389
+ const source = sources.shift();
390
+ if (isObject(target) && isObject(source)) {
391
+ for (const key in source) {
392
+ if (isObject(source[key])) {
393
+ if (typeof target[key] === 'undefined') {
394
+ component.$set(target, key, {});
395
+ }
396
+ mergeDeepReactive(component, target[key], source[key]);
397
+ } else if (Array.isArray(source[key])) {
398
+ component.$set(target, key, source[key]);
399
+ } else if (typeof source[key] === 'function') {
400
+ if (source[key].toString().indexOf('[native code]') === -1) {
401
+ target[key] = source[key];
402
+ }
403
+ } else {
404
+ component.$set(target, key, source[key]);
405
+ }
406
+ }
407
+ }
408
+ return mergeDeepReactive(component, target, ...sources);
409
+ }
410
+ /**
411
+ * Check if objects or arrays are equal by comparing nested values
412
+ *
413
+ * @param {object|array} left
414
+ * @param {object|array} right
415
+ *
416
+ * @returns {boolean}
417
+ */
418
+ export function notEqualDeep(left, right, cache = [], path = '') {
419
+ if (typeof right !== typeof left) {
420
+ return { left, right, what: path + '.typeof' };
421
+ } else if (Array.isArray(left) && !Array.isArray(right)) {
422
+ return { left, right, what: path + '.isArray' };
423
+ } else if (Array.isArray(right) && !Array.isArray(left)) {
424
+ return { left, right, what: path + '.isArray' };
425
+ } else if (Array.isArray(left) && Array.isArray(right)) {
426
+ if (left.length !== right.length) {
427
+ return { left, right, what: path + '.length' };
428
+ }
429
+ let what;
430
+ for (let index = 0, len = left.length; index < len; index++) {
431
+ if ((what = notEqualDeep(left[index], right[index], cache, path + '.' + index))) {
432
+ return what;
433
+ }
434
+ }
435
+ } else if (isObject(left) && !isObject(right)) {
436
+ return { left, right, what: path + '.isObject' };
437
+ } else if (isObject(right) && !isObject(left)) {
438
+ return { left, right, what: path + '.isObject' };
439
+ } else if (isObject(left) && isObject(right)) {
440
+ for (let key in left) {
441
+ if (
442
+ !Object.prototype.hasOwnProperty.call(left, key) ||
443
+ !Object.prototype.propertyIsEnumerable.call(left, key)
444
+ ) {
445
+ continue;
446
+ }
447
+
448
+ if (!Object.prototype.hasOwnProperty.call(right, key)) {
449
+ return { left, right, what: path + '.' + key };
450
+ }
451
+ let what;
452
+ if ((what = notEqualDeep(left[key], right[key], cache, path + '.' + key))) {
453
+ return what;
454
+ }
455
+ }
456
+ } else if (left !== right) {
457
+ return { left, right, what: path + '. !==' };
458
+ }
459
+ return false;
460
+ }
461
+
462
+ /**
463
+ * GanttElastic
464
+ * Main vue component
465
+ */
466
+ const GanttElastic = {
467
+ name: 'GanttElastic',
468
+ components: {
469
+ MainView,
470
+ 'gantt-header': GanttHeader
471
+ },
472
+ props: ['tasks', 'options', 'dynamicStyle', 'isHeaderVisible', 'projectName'],
473
+ provide() {
474
+ const provider = {};
475
+ const self = this;
476
+ Object.defineProperty(provider, 'root', {
477
+ enumerable: true,
478
+ get: () => self
479
+ });
480
+ return provider;
481
+ },
482
+ data() {
483
+ return {
484
+ state: {
485
+ tasks: [],
486
+ options: {
487
+ scrollBarHeight: 0,
488
+ allVisibleTasksHeight: 0,
489
+ outerHeight: 0,
490
+ scroll: {
491
+ left: 0,
492
+ top: 0
493
+ }
494
+ },
495
+ dynamicStyle: {},
496
+ refs: {},
497
+ tasksById: {},
498
+ taskTree: {},
499
+ ctx,
500
+ emitTasksChanges: true, // some operations may pause emitting changes to parent component
501
+ emitOptionsChanges: true, // some operations may pause emitting changes to parent component
502
+ resizeObserver: null,
503
+ unwatchTasks: null,
504
+ unwatchOptions: null,
505
+ unwatchStyle: null,
506
+ unwatchOutputTasks: null,
507
+ unwatchOutputOptions: null,
508
+ unwatchOutputStyle: null
509
+ }
510
+ };
511
+ },
512
+ methods: {
513
+ mergeDeep,
514
+ mergeDeepReactive,
515
+
516
+ /**
517
+ * Calculate height of scrollbar in current browser
518
+ *
519
+ * @returns {number}
520
+ */
521
+ getScrollBarHeight() {
522
+ const outer = document.createElement('div');
523
+ outer.style.visibility = 'hidden';
524
+ outer.style.height = '100px';
525
+ outer.style.msOverflowStyle = 'scrollbar';
526
+ document.body.appendChild(outer);
527
+ var noScroll = outer.offsetHeight;
528
+ outer.style.overflow = 'scroll';
529
+ var inner = document.createElement('div');
530
+ inner.style.height = '100%';
531
+ outer.appendChild(inner);
532
+ var withScroll = inner.offsetHeight;
533
+ outer.parentNode.removeChild(outer);
534
+ const height = noScroll - withScroll;
535
+ this.style['chart-scroll-container--vertical']['margin-left'] = `-${height}px`;
536
+ return (this.state.options.scrollBarHeight = height);
537
+ },
538
+
539
+ /**
540
+ * Fill out empty task properties and make it reactive
541
+ *
542
+ * @param {array} tasks
543
+ */
544
+ fillTasks(tasks) {
545
+ for (let task of tasks) {
546
+ if (typeof task.x === 'undefined') {
547
+ task.x = 0;
548
+ }
549
+ if (typeof task.y === 'undefined') {
550
+ task.y = 0;
551
+ }
552
+ if (typeof task.width === 'undefined') {
553
+ task.width = 0;
554
+ }
555
+ if (typeof task.height === 'undefined') {
556
+ task.height = 0;
557
+ }
558
+ if (typeof task.mouseOver === 'undefined') {
559
+ task.mouseOver = false;
560
+ }
561
+ if (typeof task.collapsed === 'undefined') {
562
+ task.collapsed = false;
563
+ }
564
+ if (typeof task.dependentOn === 'undefined') {
565
+ task.dependentOn = [];
566
+ }
567
+ if (typeof task.parentId === 'undefined') {
568
+ task.parentId = null;
569
+ }
570
+ if (typeof task.style === 'undefined') {
571
+ task.style = {};
572
+ }
573
+ if (typeof task.children === 'undefined') {
574
+ task.children = [];
575
+ }
576
+ if (typeof task.allChildren === 'undefined') {
577
+ task.allChildren = [];
578
+ }
579
+ if (typeof task.parents === 'undefined') {
580
+ task.parents = [];
581
+ }
582
+ if (typeof task.parent === 'undefined') {
583
+ task.parent = null;
584
+ }
585
+ if (typeof task.startTime === 'undefined') {
586
+ task.startTime = dayjs(task.start).valueOf();
587
+ }
588
+ if (typeof task.endTime === 'undefined' && Object.prototype.hasOwnProperty.call(task, 'end')) {
589
+ task.endTime = dayjs(task.end).valueOf();
590
+ } else if (typeof task.endTime === 'undefined' && Object.prototype.hasOwnProperty.call(task, 'duration')) {
591
+ task.endTime = task.startTime + task.duration;
592
+ }
593
+ if (typeof task.duration === 'undefined' && Object.prototype.hasOwnProperty.call(task, 'endTime')) {
594
+ task.duration = task.endTime - task.startTime;
595
+ }
596
+ }
597
+ return tasks;
598
+ },
599
+
600
+ /**
601
+ * Map tasks
602
+ *
603
+ * @param {Array} tasks
604
+ * @param {Object} options
605
+ */
606
+ mapTasks(tasks, options) {
607
+ for (let [index, task] of tasks.entries()) {
608
+ tasks[index] = {
609
+ ...task,
610
+ id: task[options.taskMapping.id],
611
+ start: task[options.taskMapping.start],
612
+ label: task[options.taskMapping.label],
613
+ duration: task[options.taskMapping.duration],
614
+ progress: task[options.taskMapping.progress],
615
+ type: task[options.taskMapping.type],
616
+ style: task[options.taskMapping.style],
617
+ collapsed: task[options.taskMapping.collapsed]
618
+ };
619
+ }
620
+ return tasks;
621
+ },
622
+
623
+ /**
624
+ * Initialize component
625
+ */
626
+ initialize(itsUpdate = '') {
627
+ let options = mergeDeep({}, this.state.options, getOptions(this.options), this.options);
628
+ let tasks = this.mapTasks(this.tasks, options);
629
+ if (Object.keys(this.state.dynamicStyle).length === 0) {
630
+ this.initializeStyle();
631
+ }
632
+ dayjs.locale(options.locale, null, true);
633
+ dayjs.locale(options.locale.name);
634
+ if (typeof options.taskList === 'undefined') {
635
+ options.taskList = {};
636
+ }
637
+ options.taskList.columns = options.taskList.columns.map((column, index) => {
638
+ column.thresholdPercent = 100;
639
+ column.widthFromPercentage = 0;
640
+ column.finalWidth = 0;
641
+ if (typeof column.height === 'undefined') {
642
+ column.height = 0;
643
+ }
644
+ if (typeof column.style === 'undefined') {
645
+ column.style = {};
646
+ }
647
+ column._id = `${index}-${column.label}`;
648
+ return column;
649
+ });
650
+ this.state.options = options;
651
+ tasks = this.fillTasks(tasks);
652
+ this.state.tasksById = this.resetTaskTree(tasks);
653
+ this.state.taskTree = this.makeTaskTree(this.state.rootTask, tasks);
654
+
655
+ // Get all visible tasks including parent tasks
656
+ const visibleTaskIds = new Set();
657
+
658
+ // Add all root-level tasks (these include parent tasks that don't have parentId)
659
+ this.state.taskTree.allChildren.forEach(childId => {
660
+ visibleTaskIds.add(childId);
661
+ });
662
+
663
+ // Also add any parent tasks that have children but might not be in root children
664
+ tasks.forEach(task => {
665
+ if (task.allChildren && task.allChildren.length > 0) {
666
+ visibleTaskIds.add(task.id);
667
+ }
668
+ });
669
+
670
+ this.state.tasks = Array.from(visibleTaskIds).map(childId => this.getTask(childId));
671
+ this.calculateTaskListColumnsDimensions();
672
+ this.state.options.scrollBarHeight = this.getScrollBarHeight();
673
+ this.state.options.outerHeight = this.state.options.height + this.state.options.scrollBarHeight;
674
+ this.globalOnResize();
675
+ },
676
+
677
+ /**
678
+ * Initialize style
679
+ */
680
+ initializeStyle() {
681
+ this.state.dynamicStyle = mergeDeep({}, prepareStyle(this.dynamicStyle), this.dynamicStyle);
682
+ },
683
+
684
+ /**
685
+ * Get calendar rows outer height
686
+ *
687
+ * @returns {int}
688
+ */
689
+ getCalendarHeight() {
690
+ return this.state.options.calendar.height + this.state.options.calendar.strokeWidth;
691
+ },
692
+
693
+ /**
694
+ * Get maximal level of nested task children
695
+ *
696
+ * @returns {int}
697
+ */
698
+ getMaximalLevel() {
699
+ let maximalLevel = 0;
700
+ this.state.tasks.forEach(task => {
701
+ if (task.parents.length > maximalLevel) {
702
+ maximalLevel = task.parents.length;
703
+ }
704
+ });
705
+ return maximalLevel - 1;
706
+ },
707
+
708
+ /**
709
+ * Get maximal expander width - to calculate straight task list text
710
+ *
711
+ * @returns {int}
712
+ */
713
+ getMaximalExpanderWidth() {
714
+ return (
715
+ this.getMaximalLevel() * this.state.options.taskList.expander.padding +
716
+ this.state.options.taskList.expander.margin
717
+ );
718
+ },
719
+
720
+ /**
721
+ * Synchronize scrollTop property when row height is changed
722
+ */
723
+ syncScrollTop() {
724
+ if (
725
+ this.state.refs.taskListItems &&
726
+ this.state.refs.chartGraph.scrollTop !== this.state.refs.taskListItems.scrollTop
727
+ ) {
728
+ this.state.options.scroll.top = this.state.refs.taskListItems.scrollTop = this.state.refs.chartScrollContainerVertical.scrollTop = this.state.refs.chartGraph.scrollTop;
729
+ }
730
+ },
731
+
732
+ /**
733
+ * Calculate task list columns dimensions
734
+ */
735
+ calculateTaskListColumnsDimensions() {
736
+ let final = 0;
737
+ let percentage = 0;
738
+ for (let column of this.state.options.taskList.columns) {
739
+ if (column.expander) {
740
+ column.widthFromPercentage =
741
+ ((this.getMaximalExpanderWidth() + column.width) / 100) * this.state.options.taskList.percent;
742
+ } else {
743
+ column.widthFromPercentage = (column.width / 100) * this.state.options.taskList.percent;
744
+ }
745
+ percentage += column.widthFromPercentage;
746
+ column.finalWidth = (column.thresholdPercent * column.widthFromPercentage) / 100;
747
+ final += column.finalWidth;
748
+ column.height = this.getTaskHeight() - this.style['grid-line-horizontal']['stroke-width'];
749
+ }
750
+ this.state.options.taskList.widthFromPercentage = percentage;
751
+ this.state.options.taskList.finalWidth = final;
752
+ },
753
+
754
+ /**
755
+ * Reset task tree - which is used to create tree like structure inside task list
756
+ */
757
+ resetTaskTree(tasks) {
758
+ this.$set(this.state, 'rootTask', {
759
+ id: null,
760
+ label: 'root',
761
+ children: [],
762
+ allChildren: [],
763
+ parents: [],
764
+ parent: null,
765
+ __root: true
766
+ });
767
+ const tasksById = {};
768
+ for (let i = 0, len = tasks.length; i < len; i++) {
769
+ let current = tasks[i];
770
+ current.children = [];
771
+ current.allChildren = [];
772
+ current.parent = null;
773
+ current.parents = [];
774
+ tasksById[current.id] = current;
775
+ }
776
+ return tasksById;
777
+ },
778
+
779
+ /**
780
+ * Make task tree, after reset - look above
781
+ *
782
+ * @param {object} task
783
+ * @returns {object} tasks with children and parents
784
+ */
785
+ makeTaskTree(task, tasks) {
786
+ for (let i = 0, len = tasks.length; i < len; i++) {
787
+ let current = tasks[i];
788
+ if (current.parentId === task.id) {
789
+ if (task.parents.length) {
790
+ task.parents.forEach(parent => current.parents.push(parent));
791
+ }
792
+ if (!Object.prototype.propertyIsEnumerable.call(task, '__root')) {
793
+ current.parents.push(task.id);
794
+ current.parent = task.id;
795
+ } else {
796
+ current.parents = [];
797
+ current.parent = null;
798
+ }
799
+ current = this.makeTaskTree(current, tasks);
800
+ task.allChildren.push(current.id);
801
+ task.children.push(current.id);
802
+ current.allChildren.forEach(childId => task.allChildren.push(childId));
803
+ }
804
+ }
805
+
806
+
807
+ return task;
808
+ },
809
+
810
+ /**
811
+ * Get task by id
812
+ *
813
+ * @param {any} taskId
814
+ * @returns {object|null} task
815
+ */
816
+ getTask(taskId) {
817
+ if (typeof this.state.tasksById[taskId] !== 'undefined') {
818
+ return this.state.tasksById[taskId];
819
+ }
820
+ return null;
821
+ },
822
+
823
+ /**
824
+ * Get children tasks for specified taskId
825
+ *
826
+ * @param {any} taskId
827
+ * @returns {array} children
828
+ */
829
+ getChildren(taskId) {
830
+ return this.state.tasks.filter(task => task.parent === taskId);
831
+ },
832
+
833
+ /**
834
+ * Is task visible
835
+ *
836
+ * @param {Number|String|Task} task
837
+ */
838
+ isTaskVisible(task) {
839
+ if (typeof task === 'number' || typeof task === 'string') {
840
+ task = this.getTask(task);
841
+ }
842
+
843
+ // Always show parent tasks (tasks with children) - they need to be visible for expander functionality
844
+ if (task.allChildren && task.allChildren.length > 0) {
845
+ return true;
846
+ }
847
+
848
+ // For child tasks, check if any parent is collapsed
849
+ for (let i = 0, len = task.parents.length; i < len; i++) {
850
+ const parentTask = this.getTask(task.parents[i]);
851
+ if (parentTask && parentTask.collapsed) {
852
+ return false;
853
+ }
854
+ }
855
+
856
+ // Show all other tasks (root-level tasks and non-collapsed child tasks)
857
+ return true;
858
+ },
859
+
860
+ /**
861
+ * Get svg
862
+ *
863
+ * @returns {string} html svg image of gantt
864
+ */
865
+ getSVG() {
866
+ return this.state.options.mainView.outerHTML;
867
+ },
868
+
869
+ /**
870
+ * Get image
871
+ *
872
+ * @param {string} type image format
873
+ * @returns {Promise} when resolved returns base64 image string of gantt
874
+ */
875
+ getImage(type = 'image/png') {
876
+ return new Promise(resolve => {
877
+ const img = new Image();
878
+ img.onload = () => {
879
+ const canvas = document.createElement('canvas');
880
+ canvas.width = this.state.options.mainView.clientWidth;
881
+ canvas.height = this.state.options.rowsHeight;
882
+ canvas.getContext('2d').drawImage(img, 0, 0);
883
+ resolve(canvas.toDataURL(type));
884
+ };
885
+ img.src = 'data:image/svg+xml,' + encodeURIComponent(this.getSVG());
886
+ });
887
+ },
888
+
889
+ /**
890
+ * Get gantt total height
891
+ *
892
+ * @returns {number}
893
+ */
894
+ getHeight(visibleTasks, outer = false) {
895
+ let height =
896
+ visibleTasks.length * (this.state.options.row.height + this.state.options.chart.grid.horizontal.gap * 2) +
897
+ this.state.options.calendar.height +
898
+ this.state.options.calendar.strokeWidth +
899
+ this.state.options.calendar.gap;
900
+ if (outer) {
901
+ height += this.state.options.scrollBarHeight;
902
+ }
903
+ return height;
904
+ },
905
+
906
+ /**
907
+ * Get one task height
908
+ *
909
+ * @returns {number}
910
+ */
911
+ getTaskHeight(withStroke = false) {
912
+ if (withStroke) {
913
+ return (
914
+ this.state.options.row.height +
915
+ this.state.options.chart.grid.horizontal.gap * 2 +
916
+ this.style['grid-line-horizontal']['stroke-width']
917
+ );
918
+ }
919
+ return this.state.options.row.height + this.state.options.chart.grid.horizontal.gap * 2;
920
+ },
921
+
922
+ /**
923
+ * Get specified tasks height
924
+ *
925
+ * @returns {number}
926
+ */
927
+ getTasksHeight(visibleTasks) {
928
+ return visibleTasks.length * this.getTaskHeight();
929
+ },
930
+
931
+ /**
932
+ * Convert time (in milliseconds) to pixel offset inside chart
933
+ *
934
+ * @param {int} ms
935
+ * @returns {number}
936
+ */
937
+ timeToPixelOffsetX(ms) {
938
+ let x = ms - this.state.options.times.firstTime;
939
+ if (x) {
940
+ x = x / this.state.options.times.timePerPixel;
941
+ }
942
+ return x;
943
+ },
944
+
945
+ /**
946
+ * Convert pixel offset inside chart to corresponding time offset in milliseconds
947
+ *
948
+ * @param {number} pixelOffsetX
949
+ * @returns {int} milliseconds
950
+ */
951
+ pixelOffsetXToTime(pixelOffsetX) {
952
+ let offset = pixelOffsetX + this.style['grid-line-vertical']['stroke-width'] / 2;
953
+ return offset * this.state.options.times.timePerPixel + this.state.options.times.firstTime;
954
+ },
955
+
956
+ /**
957
+ * Determine if element is inside current view port
958
+ *
959
+ * @param {number} x - element placement
960
+ * @param {number} width - element width
961
+ * @param {int} buffer - or threshold, if element is outside viewport but offset from view port is below this value return true
962
+ * @returns {boolean}
963
+ */
964
+ isInsideViewPort(x, width, buffer = 5000) {
965
+ return (
966
+ (x + width + buffer >= this.state.options.scroll.chart.left &&
967
+ x - buffer <= this.state.options.scroll.chart.right) ||
968
+ (x - buffer <= this.state.options.scroll.chart.left &&
969
+ x + width + buffer >= this.state.options.scroll.chart.right)
970
+ );
971
+ },
972
+
973
+ /**
974
+ * Chart scroll event handler
975
+ *
976
+ * @param {event} ev
977
+ */
978
+ onScrollChart(ev) {
979
+ this._onScrollChart(
980
+ this.state.refs.chartScrollContainerHorizontal.scrollLeft,
981
+ this.state.refs.chartScrollContainerVertical.scrollTop
982
+ );
983
+ },
984
+
985
+ /**
986
+ * After same as above but with different arguments - normalized
987
+ *
988
+ * @param {number} left
989
+ * @param {number} top
990
+ */
991
+ _onScrollChart(left, top) {
992
+ if (this.state.options.scroll.chart.left === left && this.state.options.scroll.chart.top === top) {
993
+ return;
994
+ }
995
+ const chartContainerWidth = this.state.refs.chartContainer.clientWidth;
996
+ this.state.options.scroll.chart.left = left;
997
+ this.state.options.scroll.chart.right = left + chartContainerWidth;
998
+ this.state.options.scroll.chart.percent = (left / this.state.options.times.totalViewDurationPx) * 100;
999
+ this.state.options.scroll.chart.top = top;
1000
+ this.state.options.scroll.chart.time = this.pixelOffsetXToTime(left);
1001
+ this.state.options.scroll.chart.timeCenter = this.pixelOffsetXToTime(left + chartContainerWidth / 2);
1002
+ this.state.options.scroll.chart.dateTime.left = dayjs(this.state.options.scroll.chart.time).valueOf();
1003
+ this.state.options.scroll.chart.dateTime.right = dayjs(
1004
+ this.pixelOffsetXToTime(left + this.state.refs.chart.clientWidth)
1005
+ ).valueOf();
1006
+ this.scrollTo(left, top);
1007
+ },
1008
+
1009
+ /**
1010
+ * Scroll current chart to specified time (in milliseconds)
1011
+ *
1012
+ * @param {int} time
1013
+ */
1014
+ scrollToTime(time) {
1015
+ let pos = this.timeToPixelOffsetX(time);
1016
+ const chartContainerWidth = this.state.refs.chartContainer.clientWidth;
1017
+ pos = pos - chartContainerWidth / 2;
1018
+ if (pos > this.state.options.width) {
1019
+ pos = this.state.options.width - chartContainerWidth;
1020
+ }
1021
+ this.scrollTo(pos);
1022
+ },
1023
+
1024
+ /**
1025
+ * Scroll chart or task list to specified pixel values
1026
+ *
1027
+ * @param {number|null} left
1028
+ * @param {number|null} top
1029
+ */
1030
+ scrollTo(left = null, top = null) {
1031
+ if (left !== null) {
1032
+ this.state.refs.chartCalendarContainer.scrollLeft = left;
1033
+ this.state.refs.chartGraphContainer.scrollLeft = left;
1034
+ this.state.refs.chartScrollContainerHorizontal.scrollLeft = left;
1035
+ this.state.options.scroll.left = left;
1036
+ }
1037
+ if (top !== null) {
1038
+ this.state.refs.chartScrollContainerVertical.scrollTop = top;
1039
+ this.state.refs.chartGraph.scrollTop = top;
1040
+ this.state.refs.taskListItems.scrollTop = top;
1041
+ this.state.options.scroll.top = top;
1042
+ this.syncScrollTop();
1043
+ }
1044
+ },
1045
+
1046
+ /**
1047
+ * After some actions like time zoom change we need to recompensate scroll position
1048
+ * so as a result everything will be in same place
1049
+ */
1050
+ fixScrollPos() {
1051
+ this.scrollToTime(this.state.options.scroll.chart.timeCenter);
1052
+ },
1053
+
1054
+ /**
1055
+ * Mouse wheel event handler
1056
+ */
1057
+ onWheelChart(ev) {
1058
+ if (!ev.shiftKey && ev.deltaX === 0) {
1059
+ let top = this.state.options.scroll.top + ev.deltaY;
1060
+ const chartClientHeight = this.state.options.rowsHeight;
1061
+ const scrollHeight = this.state.refs.chartGraph.scrollHeight - chartClientHeight;
1062
+ if (top < 0) {
1063
+ top = 0;
1064
+ } else if (top > scrollHeight) {
1065
+ top = scrollHeight;
1066
+ }
1067
+ this.scrollTo(null, top);
1068
+ } else if (ev.shiftKey && ev.deltaX === 0) {
1069
+ let left = this.state.options.scroll.left + ev.deltaY;
1070
+ const chartClientWidth = this.state.refs.chartScrollContainerHorizontal.clientWidth;
1071
+ const scrollWidth = this.state.refs.chartScrollContainerHorizontal.scrollWidth - chartClientWidth;
1072
+ if (left < 0) {
1073
+ left = 0;
1074
+ } else if (left > scrollWidth) {
1075
+ left = scrollWidth;
1076
+ }
1077
+ this.scrollTo(left);
1078
+ } else {
1079
+ let left = this.state.options.scroll.left + ev.deltaX;
1080
+ const chartClientWidth = this.state.refs.chartScrollContainerHorizontal.clientWidth;
1081
+ const scrollWidth = this.state.refs.chartScrollContainerHorizontal.scrollWidth - chartClientWidth;
1082
+ if (left < 0) {
1083
+ left = 0;
1084
+ } else if (left > scrollWidth) {
1085
+ left = scrollWidth;
1086
+ }
1087
+ this.scrollTo(left);
1088
+ }
1089
+ },
1090
+
1091
+ /**
1092
+ * Time zoom change event handler
1093
+ */
1094
+ onTimeZoomChange(timeZoom) {
1095
+ this.state.options.times.timeZoom = timeZoom;
1096
+ this.recalculateTimes();
1097
+ this.calculateSteps();
1098
+ this.fixScrollPos();
1099
+ },
1100
+
1101
+ /**
1102
+ * Row height change event handler
1103
+ */
1104
+ onRowHeightChange(height) {
1105
+ this.state.options.row.height = height;
1106
+ this.calculateTaskListColumnsDimensions();
1107
+ this.syncScrollTop();
1108
+ },
1109
+
1110
+ /**
1111
+ * Scope change event handler
1112
+ */
1113
+ onScopeChange(value) {
1114
+ this.state.options.scope.before = value;
1115
+ this.state.options.scope.after = value;
1116
+ this.initTimes();
1117
+ this.calculateSteps();
1118
+ this.computeCalendarWidths();
1119
+ this.fixScrollPos();
1120
+ },
1121
+
1122
+ /**
1123
+ * Task list width change event handler
1124
+ */
1125
+ onTaskListWidthChange(value) {
1126
+ this.state.options.taskList.percent = value;
1127
+ this.calculateTaskListColumnsDimensions();
1128
+ this.fixScrollPos();
1129
+ },
1130
+
1131
+ /**
1132
+ * Task list column width change event handler
1133
+ */
1134
+ onTaskListColumnWidthChange() {
1135
+ this.calculateTaskListColumnsDimensions();
1136
+ this.fixScrollPos();
1137
+ },
1138
+
1139
+ /**
1140
+ * Listen to specified event names
1141
+ */
1142
+ initializeEvents() {
1143
+ this.$on('chart-scroll-horizontal', this.onScrollChart);
1144
+ this.$on('chart-scroll-vertical', this.onScrollChart);
1145
+ this.$on('chart-wheel', this.onWheelChart);
1146
+ this.$on('times-timeZoom-change', this.onTimeZoomChange);
1147
+ this.$on('row-height-change', this.onRowHeightChange);
1148
+ this.$on('scope-change', this.onScopeChange);
1149
+ this.$on('taskList-width-change', this.onTaskListWidthChange);
1150
+ this.$on('taskList-column-width-change', this.onTaskListColumnWidthChange);
1151
+ },
1152
+
1153
+ /**
1154
+ * Get responsive step widths based on screen size
1155
+ */
1156
+ getResponsiveStepWidths(stepDuration) {
1157
+ const clientWidth = this.state.options.clientWidth;
1158
+
1159
+ // Enhanced base step widths for different screen sizes
1160
+ const baseWidths = {
1161
+ 'day': {
1162
+ mobile: 40,
1163
+ tablet: 50,
1164
+ desktopSmall: 60,
1165
+ desktopBig: 80
1166
+ },
1167
+ 'week': {
1168
+ mobile: 80,
1169
+ tablet: 100,
1170
+ desktopSmall: 120,
1171
+ desktopBig: 150
1172
+ },
1173
+ 'month': {
1174
+ mobile: 120,
1175
+ tablet: 140,
1176
+ desktopSmall: 160,
1177
+ desktopBig: 200
1178
+ },
1179
+ 'quarter': {
1180
+ mobile: 200,
1181
+ tablet: 250,
1182
+ desktopSmall: 300,
1183
+ desktopBig: 400
1184
+ }
1185
+ };
1186
+
1187
+ // Determine screen size with desktop variants
1188
+ let screenSize;
1189
+ if (clientWidth < 768) {
1190
+ screenSize = 'mobile';
1191
+ } else if (clientWidth < 1024) {
1192
+ screenSize = 'tablet';
1193
+ } else if (clientWidth < 1440) {
1194
+ screenSize = 'desktopSmall';
1195
+ } else {
1196
+ screenSize = 'desktopBig';
1197
+ }
1198
+
1199
+ // Get responsive width for the step duration
1200
+ let responsiveWidth = baseWidths[stepDuration][screenSize];
1201
+
1202
+ // Calculate dynamic width based on data content
1203
+ const dataBasedWidth = this.getDataBasedStepWidth(stepDuration, clientWidth);
1204
+
1205
+ // Use the larger of responsive width or data-based width
1206
+ return Math.max(responsiveWidth, dataBasedWidth);
1207
+ },
1208
+
1209
+ /**
1210
+ * Get step width based on data content and minimum visibility requirements
1211
+ */
1212
+ getDataBasedStepWidth(stepDuration, clientWidth) {
1213
+ const estimatedSteps = this.getEstimatedStepCount(stepDuration);
1214
+ const taskListWidth = 300; // Account for task list width
1215
+ const availableWidth = clientWidth - taskListWidth;
1216
+
1217
+ // Define minimum visibility requirements
1218
+ const minVisibilitySteps = {
1219
+ 'day': 30, // Show at least 30 days
1220
+ 'week': 12, // Show at least 12 weeks (3 months)
1221
+ 'month': 8, // Show at least 8 months
1222
+ 'quarter': 3 // Show at least 3 quarters
1223
+ };
1224
+
1225
+ // Calculate minimum steps needed for good visibility
1226
+ const minSteps = Math.max(estimatedSteps, minVisibilitySteps[stepDuration]);
1227
+
1228
+ // Define minimum readable width per step
1229
+ const minReadableWidth = {
1230
+ 'day': 30,
1231
+ 'week': 60,
1232
+ 'month': 80,
1233
+ 'quarter': 120
1234
+ };
1235
+
1236
+ // Calculate width per step to ensure minimum visibility
1237
+ const minWidthPerStep = availableWidth / minSteps;
1238
+
1239
+ // Use the larger of minimum readable width or calculated width
1240
+ const calculatedWidth = Math.max(minWidthPerStep, minReadableWidth[stepDuration]);
1241
+
1242
+ // Define maximum reasonable width per step
1243
+ const maxWidthPerStep = {
1244
+ 'day': 100,
1245
+ 'week': 200,
1246
+ 'month': 300,
1247
+ 'quarter': 500
1248
+ };
1249
+
1250
+ // Ensure we don't exceed maximum reasonable width
1251
+ return Math.min(calculatedWidth, maxWidthPerStep[stepDuration]);
1252
+ },
1253
+
1254
+ /**
1255
+ * Get maximum width multiplier based on screen size and data content
1256
+ */
1257
+ getMaxWidthMultiplier(stepDuration, stepCount) {
1258
+ const clientWidth = this.state.options.clientWidth;
1259
+
1260
+ // Base multipliers for different screen sizes
1261
+ const baseMultipliers = {
1262
+ mobile: 2, // 2x viewport width on mobile
1263
+ tablet: 2.5, // 2.5x viewport width on tablet
1264
+ desktopSmall: 3, // 3x viewport width on small desktop
1265
+ desktopBig: 4 // 4x viewport width on big desktop
1266
+ };
1267
+
1268
+ // Determine screen size
1269
+ let screenSize;
1270
+ if (clientWidth < 768) {
1271
+ screenSize = 'mobile';
1272
+ } else if (clientWidth < 1024) {
1273
+ screenSize = 'tablet';
1274
+ } else if (clientWidth < 1440) {
1275
+ screenSize = 'desktopSmall';
1276
+ } else {
1277
+ screenSize = 'desktopBig';
1278
+ }
1279
+
1280
+ let multiplier = baseMultipliers[screenSize];
1281
+
1282
+ // Adjust multiplier based on data content
1283
+ const minVisibilitySteps = {
1284
+ 'day': 30,
1285
+ 'week': 12,
1286
+ 'month': 8,
1287
+ 'quarter': 3
1288
+ };
1289
+
1290
+ // If we have more data than minimum visibility, allow more width
1291
+ if (stepCount > minVisibilitySteps[stepDuration]) {
1292
+ const dataRatio = stepCount / minVisibilitySteps[stepDuration];
1293
+ multiplier = Math.min(multiplier * (1 + dataRatio * 0.5), 6); // Max 6x viewport width
1294
+ }
1295
+
1296
+ return multiplier;
1297
+ },
1298
+
1299
+ /**
1300
+ * Get estimated step count for responsive calculations
1301
+ */
1302
+ getEstimatedStepCount(stepDuration) {
1303
+ const totalDurationMs = dayjs(this.state.options.times.lastTime).diff(
1304
+ this.state.options.times.firstTime,
1305
+ 'milliseconds'
1306
+ );
1307
+
1308
+ switch (stepDuration) {
1309
+ case 'quarter':
1310
+ return Math.ceil(dayjs(this.state.options.times.lastTime).diff(
1311
+ dayjs(this.state.options.times.firstTime), 'quarter', true
1312
+ ));
1313
+ case 'month':
1314
+ return Math.ceil(dayjs(this.state.options.times.lastTime).diff(
1315
+ dayjs(this.state.options.times.firstTime), 'month', true
1316
+ ));
1317
+ case 'week':
1318
+ return Math.ceil(dayjs(this.state.options.times.lastTime).diff(
1319
+ dayjs(this.state.options.times.firstTime), 'week', true
1320
+ ));
1321
+ case 'day':
1322
+ default:
1323
+ return Math.ceil(dayjs(this.state.options.times.lastTime).diff(
1324
+ dayjs(this.state.options.times.firstTime), 'day', true
1325
+ ));
1326
+ }
1327
+ },
1328
+
1329
+ /**
1330
+ * When some action was performed (scale change for example) - recalculate time variables
1331
+ */
1332
+ recalculateTimes() {
1333
+ const stepDuration = this.state.options.times.stepDuration;
1334
+ const timeZoom = this.state.options.times.timeZoom;
1335
+
1336
+ // Calculate base time per pixel based on step duration
1337
+ let baseTimePerPixel;
1338
+ const totalDurationMs = dayjs(this.state.options.times.lastTime).diff(
1339
+ this.state.options.times.firstTime,
1340
+ 'milliseconds'
1341
+ );
1342
+
1343
+ // Get responsive step width based on screen size and data content
1344
+ const responsiveStepWidth = this.getResponsiveStepWidths(stepDuration);
1345
+
1346
+ // Calculate how many steps we'll have
1347
+ const stepCount = this.getEstimatedStepCount(stepDuration);
1348
+
1349
+ // Define minimum visibility requirements
1350
+ const minVisibilitySteps = {
1351
+ 'day': 30, // Show at least 30 days
1352
+ 'week': 12, // Show at least 12 weeks (3 months)
1353
+ 'month': 8, // Show at least 8 months
1354
+ 'quarter': 3 // Show at least 3 quarters
1355
+ };
1356
+
1357
+ // Ensure we show at least the minimum number of steps
1358
+ const actualSteps = Math.max(stepCount, minVisibilitySteps[stepDuration]);
1359
+
1360
+ // Ensure minimum visibility for all view types
1361
+ if (stepCount < minVisibilitySteps[stepDuration]) {
1362
+ const firstTime = dayjs(this.state.options.times.firstTime);
1363
+ let lastTime;
1364
+
1365
+ switch (stepDuration) {
1366
+ case 'month':
1367
+ lastTime = firstTime.add(8, 'month');
1368
+ break;
1369
+ case 'week':
1370
+ lastTime = firstTime.add(12, 'week');
1371
+ break;
1372
+ case 'quarter':
1373
+ lastTime = firstTime.add(3, 'quarter');
1374
+ break;
1375
+ case 'day':
1376
+ default:
1377
+ lastTime = firstTime.add(30, 'day');
1378
+ break;
1379
+ }
1380
+
1381
+ this.state.options.times.lastTime = lastTime.valueOf();
1382
+ // Recalculate total duration with extended range
1383
+ const extendedDurationMs = lastTime.diff(firstTime, 'milliseconds');
1384
+ const minSteps = minVisibilitySteps[stepDuration];
1385
+ this.state.options.times.timePerPixel = extendedDurationMs / (minSteps * responsiveStepWidth);
1386
+ this.state.options.times.totalViewDurationMs = extendedDurationMs;
1387
+ this.state.options.times.totalViewDurationPx = minSteps * responsiveStepWidth;
1388
+ return; // Exit early for views with extended range
1389
+ }
1390
+
1391
+ // Calculate total width needed for all steps
1392
+ const totalWidthNeeded = actualSteps * responsiveStepWidth;
1393
+
1394
+ // Calculate time per pixel to fit the content with responsive step width
1395
+ this.state.options.times.timePerPixel = totalDurationMs / totalWidthNeeded;
1396
+
1397
+ // Apply zoom factor (higher zoom = more compressed view)
1398
+ const zoomFactor = Math.pow(0.8, (timeZoom - 17) / 10); // Adjust zoom sensitivity
1399
+ this.state.options.times.timePerPixel *= zoomFactor;
1400
+
1401
+ this.state.options.times.totalViewDurationMs = totalDurationMs;
1402
+ this.state.options.times.totalViewDurationPx =
1403
+ this.state.options.times.totalViewDurationMs / this.state.options.times.timePerPixel;
1404
+
1405
+ // Calculate final width
1406
+ const calculatedWidth = this.state.options.times.totalViewDurationPx + this.style['grid-line-vertical']['stroke-width'];
1407
+
1408
+ // Define maximum width based on screen size and data content
1409
+ const maxWidthMultiplier = this.getMaxWidthMultiplier(stepDuration, actualSteps);
1410
+ const maxWidth = this.state.options.clientWidth * maxWidthMultiplier;
1411
+
1412
+ // Ensure minimum width for scrolling when we have minimum visibility requirements
1413
+ const minWidthForScrolling = this.state.options.clientWidth * 1.2; // 20% more than viewport
1414
+ const finalWidth = Math.max(calculatedWidth, minWidthForScrolling);
1415
+
1416
+ if (finalWidth > maxWidth) {
1417
+ // Only limit width if it's truly excessive
1418
+ this.state.options.times.timePerPixel = totalDurationMs / (maxWidth - this.style['grid-line-vertical']['stroke-width']);
1419
+ this.state.options.times.totalViewDurationPx = totalDurationMs / this.state.options.times.timePerPixel;
1420
+ this.state.options.width = maxWidth;
1421
+ } else {
1422
+ this.state.options.width = finalWidth;
1423
+ }
1424
+ },
1425
+
1426
+ /**
1427
+ * Initialize time variables
1428
+ */
1429
+ initTimes() {
1430
+ // Calculate dynamic start date based on the formula
1431
+ const minDataDate = dayjs(this.state.options.times.firstTaskTime).startOf('day');
1432
+ const viewBasedStartDate = this.calculateDynamicStartDate();
1433
+
1434
+ // Use the minimum of data start date and view-based start date
1435
+ const dynamicStartDate = minDataDate.isBefore(viewBasedStartDate) ? minDataDate : viewBasedStartDate;
1436
+
1437
+ this.state.options.times.firstTime = dynamicStartDate
1438
+ .locale(this.state.options.locale.name)
1439
+ .subtract(this.state.options.scope.before, 'days')
1440
+ .startOf('day')
1441
+ .valueOf();
1442
+ this.state.options.times.lastTime = dayjs(this.state.options.times.lastTaskTime)
1443
+ .locale(this.state.options.locale.name)
1444
+ .endOf('day')
1445
+ .add(this.state.options.scope.after, 'days')
1446
+ .endOf('day')
1447
+ .valueOf();
1448
+ this.recalculateTimes();
1449
+ },
1450
+
1451
+ /**
1452
+ * Calculate steps
1453
+ * Steps are calculated based on stepDuration (day, week, month, quarter)
1454
+ * Each step contain information about time offset and pixel offset of this time inside gantt chart
1455
+ */
1456
+ calculateSteps() {
1457
+ const steps = [];
1458
+ const lastMs = dayjs(this.state.options.times.lastTime).valueOf();
1459
+ const stepDuration = this.state.options.times.stepDuration;
1460
+
1461
+ // Get the appropriate startOf method based on step duration
1462
+ const getStartOf = (date, duration) => {
1463
+ switch (duration) {
1464
+ case 'quarter':
1465
+ return date.startOf('quarter');
1466
+ case 'month':
1467
+ return date.startOf('month');
1468
+ case 'week':
1469
+ return date.startOf('week');
1470
+ case 'day':
1471
+ default:
1472
+ return date.startOf('day');
1473
+ }
1474
+ };
1475
+
1476
+ const firstDate = dayjs(this.state.options.times.firstTime);
1477
+ steps.push({
1478
+ time: firstDate.valueOf(),
1479
+ offset: {
1480
+ ms: 0,
1481
+ px: 0
1482
+ }
1483
+ });
1484
+
1485
+ let currentDate = getStartOf(firstDate.clone().add(1, stepDuration), stepDuration);
1486
+
1487
+ while (currentDate.valueOf() <= lastMs) {
1488
+ const offsetMs = currentDate.diff(this.state.options.times.firstTime, 'milliseconds');
1489
+ const offsetPx = Math.round(offsetMs / this.state.options.times.timePerPixel); // Round to avoid sub-pixel issues
1490
+ const step = {
1491
+ time: currentDate.valueOf(),
1492
+ offset: {
1493
+ ms: offsetMs,
1494
+ px: offsetPx
1495
+ }
1496
+ };
1497
+ const previousStep = steps[steps.length - 1];
1498
+ previousStep.width = {
1499
+ ms: offsetMs - previousStep.offset.ms,
1500
+ px: Math.round(offsetPx - previousStep.offset.px) // Round to avoid sub-pixel issues
1501
+ };
1502
+ steps.push(step);
1503
+
1504
+ // Move to next step
1505
+ currentDate = getStartOf(currentDate.add(1, stepDuration), stepDuration);
1506
+ }
1507
+
1508
+ const lastStep = steps[steps.length - 1];
1509
+ lastStep.width = {
1510
+ ms: this.state.options.times.totalViewDurationMs - lastStep.offset.ms,
1511
+ px: Math.round(this.state.options.times.totalViewDurationPx - lastStep.offset.px) // Round to avoid sub-pixel issues
1512
+ };
1513
+ this.state.options.times.steps = steps;
1514
+ },
1515
+
1516
+ /**
1517
+ * Calculate calendar widths - when scale was changed for example
1518
+ */
1519
+ computeCalendarWidths() {
1520
+ this.computeDayWidths();
1521
+ this.computeHourWidths();
1522
+ this.computeMonthWidths();
1523
+ },
1524
+
1525
+ /**
1526
+ * Compute width of calendar hours column widths basing on text widths
1527
+ */
1528
+ computeHourWidths() {
1529
+ const style = { ...this.style['calendar-row-text'], ...this.style['calendar-row-text--hour'] };
1530
+ this.state.ctx.font = style['font-size'] + ' ' + style['font-family'];
1531
+ const localeName = this.state.options.locale.name;
1532
+ let currentDate = dayjs('2018-01-01T00:00:00').locale(localeName); // any date will be good for hours
1533
+ let maxWidths = this.state.options.calendar.hour.maxWidths;
1534
+ if (maxWidths.length) {
1535
+ return;
1536
+ }
1537
+ for (let formatName in this.state.options.calendar.hour.format) {
1538
+ maxWidths[formatName] = 0;
1539
+ }
1540
+ for (let hour = 0; hour < 24; hour++) {
1541
+ let widths = { hour };
1542
+ for (let formatName in this.state.options.calendar.hour.format) {
1543
+ const hourFormatted = this.state.options.calendar.hour.format[formatName](currentDate);
1544
+ this.state.options.calendar.hour.formatted[formatName].push(hourFormatted);
1545
+ widths[formatName] = this.state.ctx.measureText(hourFormatted).width;
1546
+ }
1547
+ this.state.options.calendar.hour.widths.push(widths);
1548
+ for (let formatName in this.state.options.calendar.hour.format) {
1549
+ if (widths[formatName] > maxWidths[formatName]) {
1550
+ maxWidths[formatName] = widths[formatName];
1551
+ }
1552
+ }
1553
+ currentDate = currentDate.add(1, 'hour');
1554
+ }
1555
+ },
1556
+
1557
+ /**
1558
+ * Compute calendar days column widths basing on text widths
1559
+ */
1560
+ computeDayWidths() {
1561
+ const style = { ...this.style['calendar-row-text'], ...this.style['calendar-row-text--day'] };
1562
+ this.state.ctx.font = style['font-size'] + ' ' + style['font-family'];
1563
+ const localeName = this.state.options.locale.name;
1564
+ let currentDate = dayjs(this.state.options.times.steps[0].time).locale(localeName);
1565
+ let maxWidths = this.state.options.calendar.day.maxWidths;
1566
+ this.state.options.calendar.day.widths = [];
1567
+ Object.keys(this.state.options.calendar.day.format).forEach(formatName => {
1568
+ maxWidths[formatName] = 0;
1569
+ });
1570
+ for (let day = 0, daysLen = this.state.options.times.steps.length; day < daysLen; day++) {
1571
+ const widths = {
1572
+ day
1573
+ };
1574
+ Object.keys(this.state.options.calendar.day.format).forEach(formatName => {
1575
+ widths[formatName] = this.state.ctx.measureText(
1576
+ this.state.options.calendar.day.format[formatName](currentDate)
1577
+ ).width;
1578
+ });
1579
+ this.state.options.calendar.day.widths.push(widths);
1580
+ Object.keys(this.state.options.calendar.day.format).forEach(formatName => {
1581
+ if (widths[formatName] > maxWidths[formatName]) {
1582
+ maxWidths[formatName] = widths[formatName];
1583
+ }
1584
+ });
1585
+ currentDate = currentDate.add(1, 'day');
1586
+ }
1587
+ },
1588
+
1589
+ /**
1590
+ * Months count
1591
+ *
1592
+ * @description Returns number of different months in specified time range
1593
+ *
1594
+ * @param {number} fromTime - date in ms
1595
+ * @param {number} toTime - date in ms
1596
+ *
1597
+ * @returns {number} different months count
1598
+ */
1599
+ monthsCount(fromTime, toTime) {
1600
+ if (fromTime > toTime) {
1601
+ return 0;
1602
+ }
1603
+ let currentMonth = dayjs(fromTime);
1604
+ let previousMonth = currentMonth.clone();
1605
+ let monthsCount = 1;
1606
+ while (currentMonth.valueOf() <= toTime) {
1607
+ currentMonth = currentMonth.add(1, 'day');
1608
+ if (previousMonth.month() !== currentMonth.month()) {
1609
+ monthsCount++;
1610
+ }
1611
+ previousMonth = currentMonth.clone();
1612
+ }
1613
+ return monthsCount;
1614
+ },
1615
+
1616
+ /**
1617
+ * Compute month calendar columns widths basing on text widths
1618
+ */
1619
+ computeMonthWidths() {
1620
+ const style = { ...this.style['calendar-row-text'], ...this.style['calendar-row-text--month'] };
1621
+ this.state.ctx.font = style['font-size'] + ' ' + style['font-family'];
1622
+ let maxWidths = this.state.options.calendar.month.maxWidths;
1623
+ this.state.options.calendar.month.widths = [];
1624
+ Object.keys(this.state.options.calendar.month.format).forEach(formatName => {
1625
+ maxWidths[formatName] = 0;
1626
+ });
1627
+ const localeName = this.state.options.locale.name;
1628
+ let currentDate = dayjs(this.state.options.times.firstTime).locale(localeName);
1629
+ const monthsCount = this.monthsCount(this.state.options.times.firstTime, this.state.options.times.lastTime);
1630
+ for (let month = 0; month < monthsCount; month++) {
1631
+ const widths = {
1632
+ month
1633
+ };
1634
+ Object.keys(this.state.options.calendar.month.format).forEach(formatName => {
1635
+ widths[formatName] = this.state.ctx.measureText(
1636
+ this.state.options.calendar.month.format[formatName](currentDate)
1637
+ ).width;
1638
+ });
1639
+ this.state.options.calendar.month.widths.push(widths);
1640
+ Object.keys(this.state.options.calendar.month.format).forEach(formatName => {
1641
+ if (widths[formatName] > maxWidths[formatName]) {
1642
+ maxWidths[formatName] = widths[formatName];
1643
+ }
1644
+ });
1645
+ currentDate = currentDate.add(1, 'month');
1646
+ }
1647
+ },
1648
+
1649
+ /**
1650
+ * Calculate dynamic start date based on view mode and current date
1651
+ * Formula: Min((Min Date from data), case when day then Current Date when week then current date - 1 week, when month then Current Month -1 when quarter then current quarter - 1)
1652
+ */
1653
+ calculateDynamicStartDate() {
1654
+ const currentDate = dayjs();
1655
+ const stepDuration = this.state.options.times.stepDuration;
1656
+ let viewBasedStartDate;
1657
+
1658
+ switch (stepDuration) {
1659
+ case 'day':
1660
+ viewBasedStartDate = currentDate.startOf('day');
1661
+ break;
1662
+ case 'week':
1663
+ viewBasedStartDate = currentDate.subtract(1, 'week').startOf('week');
1664
+ break;
1665
+ case 'month':
1666
+ viewBasedStartDate = currentDate.subtract(1, 'month').startOf('month');
1667
+ break;
1668
+ case 'quarter':
1669
+ viewBasedStartDate = currentDate.subtract(1, 'quarter').startOf('quarter');
1670
+ break;
1671
+ default:
1672
+ viewBasedStartDate = currentDate.startOf('day');
1673
+ }
1674
+
1675
+ return viewBasedStartDate;
1676
+ },
1677
+
1678
+ /**
1679
+ * Prepare time and date variables for gantt
1680
+ */
1681
+ prepareDates() {
1682
+ let firstTaskTime = Number.MAX_SAFE_INTEGER;
1683
+ let lastTaskTime = 0;
1684
+ for (let index = 0, len = this.state.tasks.length; index < len; index++) {
1685
+ let task = this.state.tasks[index];
1686
+ if (task.startTime < firstTaskTime) {
1687
+ firstTaskTime = task.startTime;
1688
+ }
1689
+ if (task.startTime + task.duration > lastTaskTime) {
1690
+ lastTaskTime = task.startTime + task.duration;
1691
+ }
1692
+ }
1693
+ this.state.options.times.firstTaskTime = firstTaskTime;
1694
+ this.state.options.times.lastTaskTime = lastTaskTime;
1695
+
1696
+ // Calculate dynamic start date based on the formula
1697
+ const minDataDate = dayjs(firstTaskTime).startOf('day');
1698
+ const viewBasedStartDate = this.calculateDynamicStartDate();
1699
+
1700
+ // Use the minimum of data start date and view-based start date
1701
+ const dynamicStartDate = minDataDate.isBefore(viewBasedStartDate) ? minDataDate : viewBasedStartDate;
1702
+
1703
+ this.state.options.times.firstTime = dynamicStartDate
1704
+ .locale(this.state.options.locale.name)
1705
+ .subtract(this.state.options.scope.before, 'days')
1706
+ .startOf('day')
1707
+ .valueOf();
1708
+ this.state.options.times.lastTime = dayjs(lastTaskTime)
1709
+ .locale(this.state.options.locale.name)
1710
+ .endOf('day')
1711
+ .add(this.state.options.scope.after, 'days')
1712
+ .endOf('day')
1713
+ .valueOf();
1714
+ },
1715
+
1716
+ /**
1717
+ * Setup and calculate everything
1718
+ */
1719
+ setup(itsUpdate = '') {
1720
+ this.initialize(itsUpdate);
1721
+ this.prepareDates();
1722
+ this.initTimes();
1723
+ this.calculateSteps();
1724
+ this.computeCalendarWidths();
1725
+ this.state.options.taskList.width = this.state.options.taskList.columns.reduce(
1726
+ (prev, current) => {
1727
+ return { width: prev.width + current.width };
1728
+ },
1729
+ { width: 0 }
1730
+ ).width;
1731
+ },
1732
+
1733
+ /**
1734
+ * Global resize event (from window.addEventListener)
1735
+ */
1736
+ globalOnResize() {
1737
+ if (typeof this.$el === 'undefined' || !this.$el) {
1738
+ return;
1739
+ }
1740
+ this.state.options.clientWidth = this.$el.clientWidth;
1741
+ if (
1742
+ this.state.options.taskList.widthFromPercentage >
1743
+ (this.state.options.clientWidth / 100) * this.state.options.taskList.widthThreshold
1744
+ ) {
1745
+ const diff =
1746
+ this.state.options.taskList.widthFromPercentage -
1747
+ (this.state.options.clientWidth / 100) * this.state.options.taskList.widthThreshold;
1748
+ let diffPercent = 100 - (diff / this.state.options.taskList.widthFromPercentage) * 100;
1749
+ if (diffPercent < 0) {
1750
+ diffPercent = 0;
1751
+ }
1752
+ this.state.options.taskList.columns.forEach(column => {
1753
+ column.thresholdPercent = diffPercent;
1754
+ });
1755
+ } else {
1756
+ this.state.options.taskList.columns.forEach(column => {
1757
+ column.thresholdPercent = 100;
1758
+ });
1759
+ }
1760
+ this.calculateTaskListColumnsDimensions();
1761
+
1762
+ // Recalculate times when window is resized to adjust step widths
1763
+ this.recalculateTimes();
1764
+ this.calculateSteps();
1765
+ this.computeCalendarWidths();
1766
+
1767
+ this.$emit('calendar-recalculate');
1768
+ this.syncScrollTop();
1769
+ }
1770
+ },
1771
+
1772
+ computed: {
1773
+ /**
1774
+ * Get visible tasks
1775
+ * Very important method which will bring us only those tasks that are visible inside gantt chart
1776
+ * For example when task is collapsed - children of this task are not visible - we should not render them
1777
+ */
1778
+ visibleTasks() {
1779
+ const visibleTasks = this.state.tasks.filter(task => this.isTaskVisible(task));
1780
+
1781
+
1782
+ const maxRows = visibleTasks.slice(0, this.state.options.maxRows);
1783
+ this.state.options.rowsHeight = this.getTasksHeight(maxRows);
1784
+ let heightCompensation = 0;
1785
+ if (this.state.options.maxHeight && this.state.options.rowsHeight > this.state.options.maxHeight) {
1786
+ heightCompensation = this.state.options.rowsHeight - this.state.options.maxHeight;
1787
+ this.state.options.rowsHeight = this.state.options.maxHeight;
1788
+ }
1789
+ this.state.options.height = this.getHeight(maxRows) - heightCompensation;
1790
+ this.state.options.allVisibleTasksHeight = this.getTasksHeight(visibleTasks);
1791
+ this.state.options.outerHeight = this.getHeight(maxRows, true) - heightCompensation;
1792
+ let len = visibleTasks.length;
1793
+ for (let index = 0; index < len; index++) {
1794
+ let task = visibleTasks[index];
1795
+ task.width =
1796
+ task.duration / this.state.options.times.timePerPixel - this.style['grid-line-vertical']['stroke-width'];
1797
+ if (task.width < 0) {
1798
+ task.width = 0;
1799
+ }
1800
+ task.height = this.state.options.row.height;
1801
+ task.x = this.timeToPixelOffsetX(task.startTime);
1802
+ task.y =
1803
+ (this.state.options.row.height + this.state.options.chart.grid.horizontal.gap * 2) * index +
1804
+ this.state.options.chart.grid.horizontal.gap;
1805
+ }
1806
+ return visibleTasks;
1807
+ },
1808
+
1809
+ /**
1810
+ * Style shortcut
1811
+ */
1812
+ style() {
1813
+ return this.state.dynamicStyle;
1814
+ },
1815
+
1816
+ /**
1817
+ * Get columns and compute dimensions on the fly
1818
+ */
1819
+ getTaskListColumns() {
1820
+ this.calculateTaskListColumnsDimensions();
1821
+ return this.state.options.taskList.columns;
1822
+ },
1823
+
1824
+ /**
1825
+ * Tasks used for communicate with parent component
1826
+ */
1827
+ outputTasks() {
1828
+ return this.state.tasks;
1829
+ },
1830
+
1831
+ /**
1832
+ * Options used to communicate with parent component
1833
+ */
1834
+ outputOptions() {
1835
+ return this.state.options;
1836
+ }
1837
+ },
1838
+
1839
+ /**
1840
+ * Watch tasks after gantt instance is created and react when we have new kids on the block
1841
+ */
1842
+ created() {
1843
+ this.initializeEvents();
1844
+ this.setup();
1845
+ this.state.unwatchTasks = this.$watch(
1846
+ 'tasks',
1847
+ tasks => {
1848
+ const notEqual = notEqualDeep(tasks, this.outputTasks);
1849
+ if (notEqual) {
1850
+ this.setup('tasks');
1851
+ }
1852
+ },
1853
+ { deep: true }
1854
+ );
1855
+ this.state.unwatchOptions = this.$watch(
1856
+ 'options',
1857
+ opts => {
1858
+ const notEqual = notEqualDeep(opts, this.outputOptions);
1859
+ if (notEqual) {
1860
+ this.setup('options');
1861
+ }
1862
+ },
1863
+ { deep: true }
1864
+ );
1865
+ this.state.unwatchStyle = this.$watch(
1866
+ 'dynamicStyle',
1867
+ style => {
1868
+ const notEqual = notEqualDeep(style, this.dynamicStyle);
1869
+ if (notEqual) {
1870
+ this.initializeStyle();
1871
+ }
1872
+ },
1873
+ { deep: true, immediate: true }
1874
+ );
1875
+
1876
+ this.state.unwatchOutputTasks = this.$watch(
1877
+ 'outputTasks',
1878
+ tasks => {
1879
+ this.$emit('tasks-changed', tasks.map(task => task));
1880
+ },
1881
+ { deep: true }
1882
+ );
1883
+ this.state.unwatchOutputOptions = this.$watch(
1884
+ 'outputOptions',
1885
+ options => {
1886
+ this.$emit('options-changed', mergeDeep({}, options));
1887
+ },
1888
+ { deep: true }
1889
+ );
1890
+ this.state.unwatchOutputStyle = this.$watch(
1891
+ 'style',
1892
+ style => {
1893
+ this.$emit('dynamic-style-changed', mergeDeep({}, style));
1894
+ },
1895
+ { deep: true }
1896
+ );
1897
+
1898
+ this.$root.$emit('gantt-elastic-created', this);
1899
+ this.$emit('created', this);
1900
+ },
1901
+
1902
+ /**
1903
+ * Emit before-mount event
1904
+ */
1905
+ beforeMount() {
1906
+ this.$emit('before-mount', this);
1907
+ },
1908
+
1909
+ /**
1910
+ * Emit ready/mounted events and deliver this gantt instance to outside world when needed
1911
+ */
1912
+ mounted() {
1913
+ this.state.options.clientWidth = this.$el.clientWidth;
1914
+ this.state.resizeObserver = new ResizeObserver((entries, observer) => {
1915
+ this.globalOnResize();
1916
+ });
1917
+ this.state.resizeObserver.observe(this.$el.parentNode);
1918
+ this.globalOnResize();
1919
+ this.$emit('ready', this);
1920
+ this.$root.$emit('gantt-elastic-mounted', this);
1921
+ this.$emit('mounted', this);
1922
+ this.$root.$emit('gantt-elastic-ready', this);
1923
+ },
1924
+
1925
+ /**
1926
+ * Emit event when data was changed and before update (you can cleanup dom events here for example)
1927
+ */
1928
+ beforeUpdate() {
1929
+ this.$emit('before-update');
1930
+ },
1931
+
1932
+ /**
1933
+ * Emit event when gantt-elastic view was updated
1934
+ */
1935
+ updated() {
1936
+ this.$nextTick(() => {
1937
+ this.$emit('updated');
1938
+ });
1939
+ },
1940
+
1941
+ /**
1942
+ * Before destroy event - clean up
1943
+ */
1944
+ beforeDestroy() {
1945
+ this.state.resizeObserver.unobserve(this.$el.parentNode);
1946
+ this.state.unwatchTasks();
1947
+ this.state.unwatchOptions();
1948
+ this.state.unwatchStyle();
1949
+ this.state.unwatchOutputTasks();
1950
+ this.state.unwatchOutputOptions();
1951
+ this.state.unwatchOutputStyle();
1952
+ this.$emit('before-destroy');
1953
+ },
1954
+
1955
+ /**
1956
+ * Emit event after gantt-elastic was destroyed
1957
+ */
1958
+ destroyed() {
1959
+ this.$emit('destroyed');
1960
+ }
1961
+ };
1962
+ export default GanttElastic;
1963
+ </script>
1964
+
1965
+ <style>
1966
+ [class^='gantt-elastic'],
1967
+ [class*=' gantt-elastic'] {
1968
+ box-sizing: border-box;
1969
+ }
1970
+
1971
+ .gantt-elastic__main-view svg {
1972
+ display: block;
1973
+ }
1974
+
1975
+ .gantt-elastic__grid-horizontal-line,
1976
+ .gantt-elastic__grid-vertical-line {
1977
+ stroke: rgba(0, 0, 0, 0.08);
1978
+ stroke-width: 1;
1979
+ }
1980
+
1981
+ .gantt-elastic__grid-horizontal-line {
1982
+ stroke: rgba(0, 0, 0, 0.05);
1983
+ }
1984
+
1985
+ .gantt-elastic__grid-vertical-line {
1986
+ stroke: rgba(0, 0, 0, 0.1);
1987
+ }
1988
+
1989
+ foreignObject>* {
1990
+ margin: 0px;
1991
+ }
1992
+
1993
+ .gantt-elastic .p-2 {
1994
+ padding: 10rem;
1995
+ }
1996
+
1997
+ .gantt-elastic__main-view-main-container,
1998
+ .gantt-elastic__main-view-container {
1999
+ overflow: hidden;
2000
+ max-width: 100%;
2001
+ background: #fafbfc;
2002
+ border-radius: 12px;
2003
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
2004
+ }
2005
+
2006
+ .gantt-elastic__task-list-header-column:last-of-type {
2007
+ border-right: 1px solid rgba(0, 0, 0, 0.08);
2008
+ }
2009
+
2010
+ .gantt-elastic__task-list-item:last-of-type {
2011
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
2012
+ }
2013
+
2014
+ .gantt-elastic__task-list-item-value-wrapper:hover {
2015
+ overflow: visible !important;
2016
+ }
2017
+
2018
+ .gantt-elastic__task-list-item-value-wrapper:hover>.gantt-elastic__task-list-item-value-container {
2019
+ position: relative;
2020
+ overflow: visible !important;
2021
+ }
2022
+
2023
+ .gantt-elastic__task-list-item-value-wrapper:hover>.gantt-elastic__task-list-item-value {
2024
+ position: absolute;
2025
+ }
2026
+
2027
+ /* Modern scrollbar styling */
2028
+ .gantt-elastic__chart-scroll-container {
2029
+ overflow: auto !important;
2030
+ }
2031
+
2032
+ .gantt-elastic__chart-scroll-container::-webkit-scrollbar {
2033
+ width: 8px;
2034
+ height: 8px;
2035
+ }
2036
+
2037
+ .gantt-elastic__chart-scroll-container::-webkit-scrollbar-track {
2038
+ background: rgba(0, 0, 0, 0.05);
2039
+ border-radius: 4px;
2040
+ }
2041
+
2042
+ .gantt-elastic__chart-scroll-container::-webkit-scrollbar-thumb {
2043
+ background: rgba(0, 0, 0, 0.2);
2044
+ border-radius: 4px;
2045
+ transition: background 0.3s ease;
2046
+ }
2047
+
2048
+ .gantt-elastic__chart-scroll-container::-webkit-scrollbar-thumb:hover {
2049
+ background: rgba(0, 0, 0, 0.3);
2050
+ }
2051
+
2052
+ /* Ensure horizontal scroll container shows scroll bars when needed */
2053
+ .gantt-elastic__chart-scroll-container--horizontal {
2054
+ overflow-x: auto !important;
2055
+ overflow-y: hidden !important;
2056
+ }
2057
+
2058
+ /* Ensure vertical scroll container shows scroll bars when needed */
2059
+ .gantt-elastic__chart-scroll-container--vertical {
2060
+ overflow-x: hidden !important;
2061
+ overflow-y: auto !important;
2062
+ }
2063
+
2064
+ /* Calendar text positioning to avoid grid line overlap */
2065
+ .gantt-elastic__calendar-row-text {
2066
+ position: relative;
2067
+ z-index: 2;
2068
+ pointer-events: none;
2069
+ white-space: nowrap;
2070
+ overflow: hidden;
2071
+ text-overflow: ellipsis;
2072
+ }
2073
+
2074
+ .gantt-elastic__calendar-row-text--month {
2075
+ padding: 0 4px;
2076
+ box-sizing: border-box;
2077
+ }
2078
+
2079
+ .gantt-elastic__calendar-row-text--week {
2080
+ padding: 0 3px;
2081
+ box-sizing: border-box;
2082
+ }
2083
+
2084
+ .gantt-elastic__calendar-row-text--day {
2085
+ padding: 0 2px;
2086
+ box-sizing: border-box;
2087
+ }
2088
+
2089
+ .gantt-elastic__calendar-row-text--quarter {
2090
+ padding: 0 5px;
2091
+ box-sizing: border-box;
2092
+ }
2093
+
2094
+ /* Simple expander arrow icons */
2095
+ .gantt-elastic__task-list-expander-content,
2096
+ .gantt-elastic__chart-expander-content {
2097
+ transition: all 0.2s ease;
2098
+ border-radius: 2px;
2099
+ padding: 1px;
2100
+ cursor: pointer;
2101
+ }
2102
+
2103
+ .gantt-elastic__task-list-expander-content:hover,
2104
+ .gantt-elastic__chart-expander-content:hover {
2105
+ background-color: rgba(0, 0, 0, 0.05);
2106
+ transform: scale(1.05);
2107
+ }
2108
+
2109
+ .gantt-elastic__task-list-expander-arrow,
2110
+ .gantt-elastic__chart-expander-arrow {
2111
+ transition: all 0.2s ease;
2112
+ }
2113
+
2114
+ .gantt-elastic__task-list-expander-content:hover .gantt-elastic__task-list-expander-arrow,
2115
+ .gantt-elastic__chart-expander-content:hover .gantt-elastic__chart-expander-arrow {
2116
+ stroke: #666666;
2117
+ stroke-width: 1.5;
2118
+ }
2119
+
2120
+ /* Modern task list styling */
2121
+ .gantt-elastic__task-list-container {
2122
+ background: #ffffff;
2123
+ border-right: 1px solid rgba(0, 0, 0, 0.08);
2124
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
2125
+ }
2126
+
2127
+ /* Modern chart area styling */
2128
+ .gantt-elastic__chart {
2129
+ background: #ffffff;
2130
+ }
2131
+
2132
+ /* Calendar styling improvements */
2133
+ .gantt-elastic__calendar {
2134
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
2135
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
2136
+ }
2137
+
2138
+ .gantt-elastic__calendar-row {
2139
+ background: transparent;
2140
+ }
2141
+
2142
+ .gantt-elastic__calendar-row-rect {
2143
+ background: rgba(255, 255, 255, 0.7);
2144
+ border: 1px solid rgba(0, 0, 0, 0.05);
2145
+ transition: all 0.2s ease;
2146
+ }
2147
+
2148
+ .gantt-elastic__calendar-row-rect:hover {
2149
+ background: rgba(255, 255, 255, 0.9);
2150
+ border-color: rgba(0, 0, 0, 0.1);
2151
+ }
2152
+
2153
+ .gantt-elastic__calendar-row-text {
2154
+ color: #495057;
2155
+ font-weight: 500;
2156
+ text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
2157
+ }
2158
+
2159
+ /* Task styling improvements */
2160
+ .gantt-elastic__chart-row-wrapper {
2161
+ transition: all 0.2s ease;
2162
+ }
2163
+
2164
+ .gantt-elastic__chart-row-wrapper:hover {
2165
+ transform: translateY(-1px);
2166
+ filter: brightness(1.05);
2167
+ }
2168
+
2169
+ /* Responsive improvements */
2170
+ @media (max-width: 768px) {
2171
+ .gantt-elastic__main-view-main-container,
2172
+ .gantt-elastic__main-view-container {
2173
+ border-radius: 8px;
2174
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
2175
+ }
2176
+ }
2177
+
2178
+ /* Responsive design for different screen sizes */
2179
+ @media (max-width: 480px) {
2180
+ .gantt-elastic__calendar-row-text {
2181
+ font-size: 8px !important;
2182
+ }
2183
+
2184
+ .gantt-elastic__calendar-row-text--day {
2185
+ font-size: 7px !important;
2186
+ }
2187
+
2188
+ .gantt-elastic__calendar-row-text--week {
2189
+ font-size: 8px !important;
2190
+ }
2191
+
2192
+ .gantt-elastic__calendar-row-text--month {
2193
+ font-size: 9px !important;
2194
+ }
2195
+
2196
+ .gantt-elastic__calendar-row-text--quarter {
2197
+ font-size: 10px !important;
2198
+ }
2199
+ }
2200
+
2201
+ @media (max-width: 768px) {
2202
+ .gantt-elastic__calendar-row-text {
2203
+ font-size: 10px !important;
2204
+ }
2205
+
2206
+ .gantt-elastic__calendar-row-text--day {
2207
+ font-size: 9px !important;
2208
+ }
2209
+
2210
+ .gantt-elastic__calendar-row-text--week {
2211
+ font-size: 10px !important;
2212
+ }
2213
+
2214
+ .gantt-elastic__calendar-row-text--month {
2215
+ font-size: 11px !important;
2216
+ }
2217
+
2218
+ .gantt-elastic__calendar-row-text--quarter {
2219
+ font-size: 12px !important;
2220
+ }
2221
+ }
2222
+
2223
+ /* Desktop Small (1024px - 1439px) */
2224
+ @media (min-width: 1024px) and (max-width: 1439px) {
2225
+ .gantt-elastic__calendar-row-text {
2226
+ font-size: 12px !important;
2227
+ }
2228
+
2229
+ .gantt-elastic__calendar-row-text--day {
2230
+ font-size: 11px !important;
2231
+ }
2232
+
2233
+ .gantt-elastic__calendar-row-text--week {
2234
+ font-size: 12px !important;
2235
+ }
2236
+
2237
+ .gantt-elastic__calendar-row-text--month {
2238
+ font-size: 13px !important;
2239
+ }
2240
+
2241
+ .gantt-elastic__calendar-row-text--quarter {
2242
+ font-size: 14px !important;
2243
+ }
2244
+ }
2245
+
2246
+ /* Desktop Big (1440px+) */
2247
+ @media (min-width: 1440px) {
2248
+ .gantt-elastic__calendar-row-text {
2249
+ font-size: 14px !important;
2250
+ }
2251
+
2252
+ .gantt-elastic__calendar-row-text--day {
2253
+ font-size: 13px !important;
2254
+ }
2255
+
2256
+ .gantt-elastic__calendar-row-text--week {
2257
+ font-size: 14px !important;
2258
+ }
2259
+
2260
+ .gantt-elastic__calendar-row-text--month {
2261
+ font-size: 15px !important;
2262
+ }
2263
+
2264
+ .gantt-elastic__calendar-row-text--quarter {
2265
+ font-size: 16px !important;
2266
+ }
2267
+ }
2268
+
2269
+ /* Dark mode support */
2270
+ @media (prefers-color-scheme: dark) {
2271
+ .gantt-elastic__main-view-main-container,
2272
+ .gantt-elastic__main-view-container {
2273
+ background: #1a1a1a;
2274
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
2275
+ }
2276
+
2277
+ .gantt-elastic__task-list-container {
2278
+ background: #2d2d2d;
2279
+ border-right-color: rgba(255, 255, 255, 0.1);
2280
+ }
2281
+
2282
+ .gantt-elastic__chart {
2283
+ background: #2d2d2d;
2284
+ }
2285
+
2286
+ .gantt-elastic__calendar {
2287
+ background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%);
2288
+ border-bottom-color: rgba(255, 255, 255, 0.1);
2289
+ }
2290
+
2291
+ .gantt-elastic__calendar-row-rect {
2292
+ background: rgba(255, 255, 255, 0.05);
2293
+ border-color: rgba(255, 255, 255, 0.1);
2294
+ }
2295
+
2296
+ .gantt-elastic__calendar-row-text {
2297
+ color: #e9ecef;
2298
+ }
2299
+
2300
+ .gantt-elastic__grid-horizontal-line,
2301
+ .gantt-elastic__grid-vertical-line {
2302
+ stroke: rgba(255, 255, 255, 0.1);
2303
+ }
2304
+ }
2305
+ </style>