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,1646 @@
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
+ import MainView from './components/MainView.vue';
14
+ import getStyle from './style.js';
15
+ import ResizeObserver from 'resize-observer-polyfill';
16
+ import GanttHeader from './components/Header/Header.vue';
17
+
18
+
19
+ const ctx = document.createElement('canvas').getContext('2d');
20
+ // let VueInst = VueInstance;
21
+ // function initVue() {
22
+ // if (typeof Vue !== 'undefined' && typeof VueInst === 'undefined') {
23
+ // VueInst = Vue;
24
+ // }
25
+ // }
26
+ // initVue();
27
+
28
+ // let hourWidthCache = null;
29
+
30
+ /**
31
+ * Helper function to fill out empty options in user settings
32
+ *
33
+ * @param {object} userOptions - initial user options that will merge with those below
34
+ * @returns {object} merged options
35
+ */
36
+ function getOptions(userOptions) {
37
+ let localeName = 'en';
38
+ if (typeof userOptions.locale !== 'undefined' && typeof userOptions.locale.name !== 'undefined') {
39
+ localeName = userOptions.locale.name;
40
+ }
41
+ return {
42
+ slots: {
43
+ header: {}
44
+ },
45
+ taskMapping: {
46
+ //*
47
+ id: 'id',
48
+ start: 'start',
49
+ label: 'label',
50
+ duration: 'duration',
51
+ progress: 'progress',
52
+ type: 'type',
53
+ style: 'style',
54
+ collapsed: 'collapsed'
55
+ },
56
+ width: 0,
57
+ height: 0,
58
+ clientWidth: 0,
59
+ outerHeight: 0,
60
+ rowsHeight: 0,
61
+ allVisibleTasksHeight: 0,
62
+ scroll: {
63
+ scrolling: false,
64
+ dragXMoveMultiplier: 3, //*
65
+ dragYMoveMultiplier: 2, //*
66
+ top: 0,
67
+ taskList: {
68
+ left: 0,
69
+ right: 0,
70
+ top: 0,
71
+ bottom: 0
72
+ },
73
+ chart: {
74
+ left: 0,
75
+ right: 0,
76
+ percent: 0,
77
+ timePercent: 0,
78
+ top: 0,
79
+ bottom: 0,
80
+ time: 0,
81
+ timeCenter: 0,
82
+ dateTime: {
83
+ left: '',
84
+ right: ''
85
+ }
86
+ }
87
+ },
88
+ scope: {
89
+ //*
90
+ before: 1,
91
+ after: 1
92
+ },
93
+ times: {
94
+ timeScale: 60 * 1000,
95
+ timeZoom: 17, //*
96
+ timePerPixel: 0,
97
+ firstTime: null,
98
+ lastTime: null,
99
+ firstTaskTime: 0,
100
+ lastTaskTime: 0,
101
+ totalViewDurationMs: 0,
102
+ totalViewDurationPx: 0,
103
+ stepDuration: 'day',
104
+ steps: []
105
+ },
106
+ row: {
107
+ height: 24 //*
108
+ },
109
+ maxRows: 20, //*
110
+ maxHeight: 0, //*
111
+ chart: {
112
+ grid: {
113
+ horizontal: {
114
+ gap: 6 //*
115
+ }
116
+ },
117
+ progress: {
118
+ width: 20, //*
119
+ height: 6, //*
120
+ pattern: true,
121
+ bar: false
122
+ },
123
+ text: {
124
+ offset: 4, //*
125
+ xPadding: 10, //*
126
+ display: true //*
127
+ },
128
+ expander: {
129
+ type: 'chart',
130
+ display: false, //*
131
+ displayIfTaskListHidden: true, //*
132
+ offset: 4, //*
133
+ size: 18
134
+ }
135
+ },
136
+ taskList: {
137
+ display: true, //*
138
+ resizeAfterThreshold: true, //*
139
+ widthThreshold: 75, //*
140
+ columns: [
141
+ //*
142
+ {
143
+ id: 0,
144
+ label: 'ID',
145
+ value: 'id',
146
+ width: 40
147
+ }
148
+ ],
149
+ percent: 100, //*
150
+ width: 0,
151
+ finalWidth: 0,
152
+ widthFromPercentage: 0,
153
+ minWidth: 18,
154
+ expander: {
155
+ type: 'task-list',
156
+ size: 16,
157
+ columnWidth: 24,
158
+ padding: 16,
159
+ margin: 10,
160
+ straight: false
161
+ }
162
+ },
163
+ calendar: {
164
+ workingDays: [1, 2, 3, 4, 5], //*
165
+ gap: 6, //*
166
+ height: 0,
167
+ strokeWidth: 1,
168
+ hour: {
169
+ height: 20, //*
170
+ display: true, //*
171
+ widths: [],
172
+ maxWidths: { short: 0, medium: 0, long: 0 },
173
+ formatted: {
174
+ long: [],
175
+ medium: [],
176
+ short: []
177
+ },
178
+ format: {
179
+ //*
180
+ long(date) {
181
+ return date.format('HH:mm');
182
+ },
183
+ medium(date) {
184
+ return date.format('HH:mm');
185
+ },
186
+ short(date) {
187
+ return date.format('HH');
188
+ }
189
+ }
190
+ },
191
+ day: {
192
+ height: 20, //*
193
+ display: true, //*
194
+ widths: [],
195
+ maxWidths: { short: 0, medium: 0, long: 0 },
196
+ format: {
197
+ long(date) {
198
+ return date.format('DD dddd');
199
+ },
200
+ medium(date) {
201
+ return date.format('DD ddd');
202
+ },
203
+ short(date) {
204
+ return date.format('DD');
205
+ }
206
+ }
207
+ },
208
+ month: {
209
+ height: 20, //*
210
+ display: true, //*
211
+ widths: [],
212
+ maxWidths: { short: 0, medium: 0, long: 0 },
213
+ format: {
214
+ //*
215
+ short(date) {
216
+ return date.format('MM');
217
+ },
218
+ medium(date) {
219
+ return date.format("MMM 'YY");
220
+ },
221
+ long(date) {
222
+ return date.format('MMMM YYYY');
223
+ }
224
+ }
225
+ }
226
+ },
227
+ locale: {
228
+ //*
229
+ name: 'en',
230
+ weekdays: 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
231
+ weekdaysShort: 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
232
+ weekdaysMin: 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
233
+ months: 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
234
+ monthsShort: 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
235
+ weekStart: 1,
236
+ relativeTime: {
237
+ future: 'in %s',
238
+ past: '%s ago',
239
+ s: 'a few seconds',
240
+ m: 'a minute',
241
+ mm: '%d minutes',
242
+ h: 'an hour',
243
+ hh: '%d hours',
244
+ d: 'a day',
245
+ dd: '%d days',
246
+ M: 'a month',
247
+ MM: '%d months',
248
+ y: 'a year',
249
+ yy: '%d years'
250
+ },
251
+ formats: {
252
+ LT: 'HH:mm',
253
+ LTS: 'HH:mm:ss',
254
+ L: 'DD/MM/YYYY',
255
+ LL: 'D MMMM YYYY',
256
+ LLL: 'D MMMM YYYY HH:mm',
257
+ LLLL: 'dddd, D MMMM YYYY HH:mm'
258
+ },
259
+ ordinal: n => {
260
+ const s = ['th', 'st', 'nd', 'rd'];
261
+ const v = n % 100;
262
+ return `[${n}${s[(v - 20) % 10] || s[v] || s[0]}]`;
263
+ }
264
+ }
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Prepare style
270
+ *
271
+ * @returns {object}
272
+ */
273
+ function prepareStyle(userStyle) {
274
+ let fontSize = '12px';
275
+ let fontFamily = window
276
+ .getComputedStyle(document.body)
277
+ .getPropertyValue('font-family')
278
+ .toString();
279
+ if (typeof userStyle !== 'undefined') {
280
+ if (typeof userStyle.fontSize !== 'undefined') {
281
+ fontSize = userStyle.fontSize;
282
+ }
283
+ if (typeof userStyle.fontFamily !== 'undefined') {
284
+ fontFamily = userStyle.fontFamily;
285
+ }
286
+ }
287
+ return getStyle(fontSize, fontFamily);
288
+ }
289
+
290
+ /**
291
+ * Helper function to determine if specified variable is an object
292
+ *
293
+ * @param {any} item
294
+ *
295
+ * @returns {boolean}
296
+ */
297
+ function isObject(item) {
298
+ return (
299
+ item &&
300
+ typeof item === 'object' &&
301
+ !Array.isArray(item) &&
302
+ !(item instanceof HTMLElement) &&
303
+ !(item instanceof CanvasRenderingContext2D) &&
304
+ typeof item !== 'function'
305
+ );
306
+ }
307
+
308
+ /**
309
+ * Helper function which will merge objects recursively - creating brand new one - like clone
310
+ *
311
+ * @param {object} target
312
+ * @params {object} sources
313
+ *
314
+ * @returns {object}
315
+ */
316
+ export function mergeDeep(target, ...sources) {
317
+ if (!sources.length) {
318
+ return target;
319
+ }
320
+ const source = sources.shift();
321
+ if (isObject(target) && isObject(source)) {
322
+ for (const key in source) {
323
+ if (isObject(source[key])) {
324
+ if (typeof target[key] === 'undefined') {
325
+ target[key] = {};
326
+ }
327
+ target[key] = mergeDeep(target[key], source[key]);
328
+ } else if (Array.isArray(source[key])) {
329
+ target[key] = [];
330
+ for (let item of source[key]) {
331
+ if (isObject(item)) {
332
+ target[key].push(mergeDeep({}, item));
333
+ continue;
334
+ }
335
+ target[key].push(item);
336
+ }
337
+ } else {
338
+ target[key] = source[key];
339
+ }
340
+ }
341
+ }
342
+ return mergeDeep(target, ...sources);
343
+ }
344
+
345
+ /**
346
+ * Detect if object or array is observable
347
+ *
348
+ * @param {object|array} obj
349
+ *
350
+ * @returns {boolean}
351
+ */
352
+ function isObservable(obj) {
353
+ return typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, '__ob__');
354
+ }
355
+
356
+
357
+ /**
358
+ * Same as above but with reactivity in mind
359
+ *
360
+ * @param {object} target
361
+ * @params {object} sources
362
+ *
363
+ * @returns {object}
364
+ */
365
+ export function mergeDeepReactive(component, target, ...sources) {
366
+ if (!sources.length) {
367
+ return target;
368
+ }
369
+ const source = sources.shift();
370
+ if (isObject(target) && isObject(source)) {
371
+ for (const key in source) {
372
+ if (isObject(source[key])) {
373
+ if (typeof target[key] === 'undefined') {
374
+ component.$set(target, key, {});
375
+ }
376
+ mergeDeepReactive(component, target[key], source[key]);
377
+ } else if (Array.isArray(source[key])) {
378
+ component.$set(target, key, source[key]);
379
+ } else if (typeof source[key] === 'function') {
380
+ if (source[key].toString().indexOf('[native code]') === -1) {
381
+ target[key] = source[key];
382
+ }
383
+ } else {
384
+ component.$set(target, key, source[key]);
385
+ }
386
+ }
387
+ }
388
+ return mergeDeepReactive(component, target, ...sources);
389
+ }
390
+ /**
391
+ * Check if objects or arrays are equal by comparing nested values
392
+ *
393
+ * @param {object|array} left
394
+ * @param {object|array} right
395
+ *
396
+ * @returns {boolean}
397
+ */
398
+ export function notEqualDeep(left, right, cache = [], path = '') {
399
+ if (typeof right !== typeof left) {
400
+ return { left, right, what: path + '.typeof' };
401
+ } else if (Array.isArray(left) && !Array.isArray(right)) {
402
+ return { left, right, what: path + '.isArray' };
403
+ } else if (Array.isArray(right) && !Array.isArray(left)) {
404
+ return { left, right, what: path + '.isArray' };
405
+ } else if (Array.isArray(left) && Array.isArray(right)) {
406
+ if (left.length !== right.length) {
407
+ return { left, right, what: path + '.length' };
408
+ }
409
+ let what;
410
+ for (let index = 0, len = left.length; index < len; index++) {
411
+ if ((what = notEqualDeep(left[index], right[index], cache, path + '.' + index))) {
412
+ return what;
413
+ }
414
+ }
415
+ } else if (isObject(left) && !isObject(right)) {
416
+ return { left, right, what: path + '.isObject' };
417
+ } else if (isObject(right) && !isObject(left)) {
418
+ return { left, right, what: path + '.isObject' };
419
+ } else if (isObject(left) && isObject(right)) {
420
+ for (let key in left) {
421
+ if (
422
+ !Object.prototype.hasOwnProperty.call(left, key) ||
423
+ !Object.prototype.propertyIsEnumerable.call(left, key)
424
+ ) {
425
+ continue;
426
+ }
427
+
428
+ if (!Object.prototype.hasOwnProperty.call(right, key)) {
429
+ return { left, right, what: path + '.' + key };
430
+ }
431
+ let what;
432
+ if ((what = notEqualDeep(left[key], right[key], cache, path + '.' + key))) {
433
+ return what;
434
+ }
435
+ }
436
+ } else if (left !== right) {
437
+ return { left, right, what: path + '. !==' };
438
+ }
439
+ return false;
440
+ }
441
+
442
+ /**
443
+ * GanttElastic
444
+ * Main vue component
445
+ */
446
+ const GanttElastic = {
447
+ name: 'GanttElastic',
448
+ components: {
449
+ MainView,
450
+ 'gantt-header': GanttHeader
451
+ },
452
+ props: ['tasks', 'options', 'dynamicStyle', 'isHeaderVisible', 'projectName'],
453
+ provide() {
454
+ const provider = {};
455
+ const self = this;
456
+ Object.defineProperty(provider, 'root', {
457
+ enumerable: true,
458
+ get: () => self
459
+ });
460
+ return provider;
461
+ },
462
+ data() {
463
+ return {
464
+ state: {
465
+ tasks: [],
466
+ options: {
467
+ scrollBarHeight: 0,
468
+ allVisibleTasksHeight: 0,
469
+ outerHeight: 0,
470
+ scroll: {
471
+ left: 0,
472
+ top: 0
473
+ }
474
+ },
475
+ dynamicStyle: {},
476
+ refs: {},
477
+ tasksById: {},
478
+ taskTree: {},
479
+ ctx: ctx,
480
+ emitTasksChanges: true, // some operations may pause emitting changes to parent component
481
+ emitOptionsChanges: true, // some operations may pause emitting changes to parent component
482
+ resizeObserver: null,
483
+ unwatchTasks: null,
484
+ unwatchOptions: null,
485
+ unwatchStyle: null,
486
+ unwatchOutputTasks: null,
487
+ unwatchOutputOptions: null,
488
+ unwatchOutputStyle: null
489
+ }
490
+ };
491
+ },
492
+ methods: {
493
+ mergeDeep,
494
+ mergeDeepReactive,
495
+
496
+ /**
497
+ * Calculate height of scrollbar in current browser
498
+ *
499
+ * @returns {number}
500
+ */
501
+ getScrollBarHeight() {
502
+ const outer = document.createElement('div');
503
+ outer.style.visibility = 'hidden';
504
+ outer.style.height = '100px';
505
+ outer.style.msOverflowStyle = 'scrollbar';
506
+ document.body.appendChild(outer);
507
+ var noScroll = outer.offsetHeight;
508
+ outer.style.overflow = 'scroll';
509
+ var inner = document.createElement('div');
510
+ inner.style.height = '100%';
511
+ outer.appendChild(inner);
512
+ var withScroll = inner.offsetHeight;
513
+ outer.parentNode.removeChild(outer);
514
+ const height = noScroll - withScroll;
515
+ this.style['chart-scroll-container--vertical']['margin-left'] = `-${height}px`;
516
+ return (this.state.options.scrollBarHeight = height);
517
+ },
518
+
519
+ /**
520
+ * Fill out empty task properties and make it reactive
521
+ *
522
+ * @param {array} tasks
523
+ */
524
+ fillTasks(tasks) {
525
+ for (let task of tasks) {
526
+ if (typeof task.x === 'undefined') {
527
+ task.x = 0;
528
+ }
529
+ if (typeof task.y === 'undefined') {
530
+ task.y = 0;
531
+ }
532
+ if (typeof task.width === 'undefined') {
533
+ task.width = 0;
534
+ }
535
+ if (typeof task.height === 'undefined') {
536
+ task.height = 0;
537
+ }
538
+ if (typeof task.mouseOver === 'undefined') {
539
+ task.mouseOver = false;
540
+ }
541
+ if (typeof task.collapsed === 'undefined') {
542
+ task.collapsed = false;
543
+ }
544
+ if (typeof task.dependentOn === 'undefined') {
545
+ task.dependentOn = [];
546
+ }
547
+ if (typeof task.parentId === 'undefined') {
548
+ task.parentId = null;
549
+ }
550
+ if (typeof task.style === 'undefined') {
551
+ task.style = {};
552
+ }
553
+ if (typeof task.children === 'undefined') {
554
+ task.children = [];
555
+ }
556
+ if (typeof task.allChildren === 'undefined') {
557
+ task.allChildren = [];
558
+ }
559
+ if (typeof task.parents === 'undefined') {
560
+ task.parents = [];
561
+ }
562
+ if (typeof task.parent === 'undefined') {
563
+ task.parent = null;
564
+ }
565
+ if (typeof task.startTime === 'undefined') {
566
+ task.startTime = dayjs(task.start).valueOf();
567
+ }
568
+ if (typeof task.endTime === 'undefined' && Object.prototype.hasOwnProperty.call(task, 'end')) {
569
+ task.endTime = dayjs(task.end).valueOf();
570
+ } else if (typeof task.endTime === 'undefined' && Object.prototype.hasOwnProperty.call(task, 'duration')) {
571
+ task.endTime = task.startTime + task.duration;
572
+ }
573
+ if (typeof task.duration === 'undefined' && Object.prototype.hasOwnProperty.call(task, 'endTime')) {
574
+ task.duration = task.endTime - task.startTime;
575
+ }
576
+ }
577
+ return tasks;
578
+ },
579
+
580
+ /**
581
+ * Map tasks
582
+ *
583
+ * @param {Array} tasks
584
+ * @param {Object} options
585
+ */
586
+ mapTasks(tasks, options) {
587
+ for (let [index, task] of tasks.entries()) {
588
+ tasks[index] = {
589
+ ...task,
590
+ id: task[options.taskMapping.id],
591
+ start: task[options.taskMapping.start],
592
+ label: task[options.taskMapping.label],
593
+ duration: task[options.taskMapping.duration],
594
+ progress: task[options.taskMapping.progress],
595
+ type: task[options.taskMapping.type],
596
+ style: task[options.taskMapping.style],
597
+ collapsed: task[options.taskMapping.collapsed]
598
+ };
599
+ }
600
+ return tasks;
601
+ },
602
+
603
+ /**
604
+ * Initialize component
605
+ */
606
+ initialize(itsUpdate = '') {
607
+ let options = mergeDeep({}, this.state.options, getOptions(this.options), this.options);
608
+ let tasks = this.mapTasks(this.tasks, options);
609
+ if (Object.keys(this.state.dynamicStyle).length === 0) {
610
+ this.initializeStyle();
611
+ }
612
+ dayjs.locale(options.locale, null, true);
613
+ dayjs.locale(options.locale.name);
614
+ if (typeof options.taskList === 'undefined') {
615
+ options.taskList = {};
616
+ }
617
+ options.taskList.columns = options.taskList.columns.map((column, index) => {
618
+ column.thresholdPercent = 100;
619
+ column.widthFromPercentage = 0;
620
+ column.finalWidth = 0;
621
+ if (typeof column.height === 'undefined') {
622
+ column.height = 0;
623
+ }
624
+ if (typeof column.style === 'undefined') {
625
+ column.style = {};
626
+ }
627
+ column._id = `${index}-${column.label}`;
628
+ return column;
629
+ });
630
+ this.state.options = options;
631
+ tasks = this.fillTasks(tasks);
632
+ this.state.tasksById = this.resetTaskTree(tasks);
633
+ this.state.taskTree = this.makeTaskTree(this.state.rootTask, tasks);
634
+ this.state.tasks = this.state.taskTree.allChildren.map(childId => this.getTask(childId));
635
+ this.calculateTaskListColumnsDimensions();
636
+ this.state.options.scrollBarHeight = this.getScrollBarHeight();
637
+ this.state.options.outerHeight = this.state.options.height + this.state.options.scrollBarHeight;
638
+ this.globalOnResize();
639
+ },
640
+
641
+ /**
642
+ * Initialize style
643
+ */
644
+ initializeStyle() {
645
+ this.state.dynamicStyle = mergeDeep({}, prepareStyle(this.dynamicStyle), this.dynamicStyle);
646
+ },
647
+
648
+ /**
649
+ * Get calendar rows outer height
650
+ *
651
+ * @returns {int}
652
+ */
653
+ getCalendarHeight() {
654
+ return this.state.options.calendar.height + this.state.options.calendar.strokeWidth;
655
+ },
656
+
657
+ /**
658
+ * Get maximal level of nested task children
659
+ *
660
+ * @returns {int}
661
+ */
662
+ getMaximalLevel() {
663
+ let maximalLevel = 0;
664
+ this.state.tasks.forEach(task => {
665
+ if (task.parents.length > maximalLevel) {
666
+ maximalLevel = task.parents.length;
667
+ }
668
+ });
669
+ return maximalLevel - 1;
670
+ },
671
+
672
+ /**
673
+ * Get maximal expander width - to calculate straight task list text
674
+ *
675
+ * @returns {int}
676
+ */
677
+ getMaximalExpanderWidth() {
678
+ return (
679
+ this.getMaximalLevel() * this.state.options.taskList.expander.padding +
680
+ this.state.options.taskList.expander.margin
681
+ );
682
+ },
683
+
684
+ /**
685
+ * Synchronize scrollTop property when row height is changed
686
+ */
687
+ syncScrollTop() {
688
+ if (
689
+ this.state.refs.taskListItems &&
690
+ this.state.refs.chartGraph.scrollTop !== this.state.refs.taskListItems.scrollTop
691
+ ) {
692
+ this.state.options.scroll.top = this.state.refs.taskListItems.scrollTop = this.state.refs.chartScrollContainerVertical.scrollTop = this.state.refs.chartGraph.scrollTop;
693
+ }
694
+ },
695
+
696
+ /**
697
+ * Calculate task list columns dimensions
698
+ */
699
+ calculateTaskListColumnsDimensions() {
700
+ let final = 0;
701
+ let percentage = 0;
702
+ for (let column of this.state.options.taskList.columns) {
703
+ if (column.expander) {
704
+ column.widthFromPercentage =
705
+ ((this.getMaximalExpanderWidth() + column.width) / 100) * this.state.options.taskList.percent;
706
+ } else {
707
+ column.widthFromPercentage = (column.width / 100) * this.state.options.taskList.percent;
708
+ }
709
+ percentage += column.widthFromPercentage;
710
+ column.finalWidth = (column.thresholdPercent * column.widthFromPercentage) / 100;
711
+ final += column.finalWidth;
712
+ column.height = this.getTaskHeight() - this.style['grid-line-horizontal']['stroke-width'];
713
+ }
714
+ this.state.options.taskList.widthFromPercentage = percentage;
715
+ this.state.options.taskList.finalWidth = final;
716
+ },
717
+
718
+ /**
719
+ * Reset task tree - which is used to create tree like structure inside task list
720
+ */
721
+ resetTaskTree(tasks) {
722
+ this.$set(this.state, 'rootTask', {
723
+ id: null,
724
+ label: 'root',
725
+ children: [],
726
+ allChildren: [],
727
+ parents: [],
728
+ parent: null,
729
+ __root: true
730
+ });
731
+ const tasksById = {};
732
+ for (let i = 0, len = tasks.length; i < len; i++) {
733
+ let current = tasks[i];
734
+ current.children = [];
735
+ current.allChildren = [];
736
+ current.parent = null;
737
+ current.parents = [];
738
+ tasksById[current.id] = current;
739
+ }
740
+ return tasksById;
741
+ },
742
+
743
+ /**
744
+ * Make task tree, after reset - look above
745
+ *
746
+ * @param {object} task
747
+ * @returns {object} tasks with children and parents
748
+ */
749
+ makeTaskTree(task, tasks) {
750
+ for (let i = 0, len = tasks.length; i < len; i++) {
751
+ let current = tasks[i];
752
+ if (current.parentId === task.id) {
753
+ if (task.parents.length) {
754
+ task.parents.forEach(parent => current.parents.push(parent));
755
+ }
756
+ if (!Object.prototype.propertyIsEnumerable.call(task, '__root')) {
757
+ current.parents.push(task.id);
758
+ current.parent = task.id;
759
+ } else {
760
+ current.parents = [];
761
+ current.parent = null;
762
+ }
763
+ current = this.makeTaskTree(current, tasks);
764
+ task.allChildren.push(current.id);
765
+ task.children.push(current.id);
766
+ current.allChildren.forEach(childId => task.allChildren.push(childId));
767
+ }
768
+ }
769
+ return task;
770
+ },
771
+
772
+ /**
773
+ * Get task by id
774
+ *
775
+ * @param {any} taskId
776
+ * @returns {object|null} task
777
+ */
778
+ getTask(taskId) {
779
+ if (typeof this.state.tasksById[taskId] !== 'undefined') {
780
+ return this.state.tasksById[taskId];
781
+ }
782
+ return null;
783
+ },
784
+
785
+ /**
786
+ * Get children tasks for specified taskId
787
+ *
788
+ * @param {any} taskId
789
+ * @returns {array} children
790
+ */
791
+ getChildren(taskId) {
792
+ return this.state.tasks.filter(task => task.parent === taskId);
793
+ },
794
+
795
+ /**
796
+ * Is task visible
797
+ *
798
+ * @param {Number|String|Task} task
799
+ */
800
+ isTaskVisible(task) {
801
+ if (typeof task === 'number' || typeof task === 'string') {
802
+ task = this.getTask(task);
803
+ }
804
+ for (let i = 0, len = task.parents.length; i < len; i++) {
805
+ if (this.getTask(task.parents[i]).collapsed) {
806
+ return false;
807
+ }
808
+ }
809
+ return true;
810
+ },
811
+
812
+ /**
813
+ * Get svg
814
+ *
815
+ * @returns {string} html svg image of gantt
816
+ */
817
+ getSVG() {
818
+ return this.state.options.mainView.outerHTML;
819
+ },
820
+
821
+ /**
822
+ * Get image
823
+ *
824
+ * @param {string} type image format
825
+ * @returns {Promise} when resolved returns base64 image string of gantt
826
+ */
827
+ getImage(type = 'image/png') {
828
+ return new Promise(resolve => {
829
+ const img = new Image();
830
+ img.onload = () => {
831
+ const canvas = document.createElement('canvas');
832
+ canvas.width = this.state.options.mainView.clientWidth;
833
+ canvas.height = this.state.options.rowsHeight;
834
+ canvas.getContext('2d').drawImage(img, 0, 0);
835
+ resolve(canvas.toDataURL(type));
836
+ };
837
+ img.src = 'data:image/svg+xml,' + encodeURIComponent(this.getSVG());
838
+ });
839
+ },
840
+
841
+ /**
842
+ * Get gantt total height
843
+ *
844
+ * @returns {number}
845
+ */
846
+ getHeight(visibleTasks, outer = false) {
847
+ let height =
848
+ visibleTasks.length * (this.state.options.row.height + this.state.options.chart.grid.horizontal.gap * 2) +
849
+ this.state.options.calendar.height +
850
+ this.state.options.calendar.strokeWidth +
851
+ this.state.options.calendar.gap;
852
+ if (outer) {
853
+ height += this.state.options.scrollBarHeight;
854
+ }
855
+ return height;
856
+ },
857
+
858
+ /**
859
+ * Get one task height
860
+ *
861
+ * @returns {number}
862
+ */
863
+ getTaskHeight(withStroke = false) {
864
+ if (withStroke) {
865
+ return (
866
+ this.state.options.row.height +
867
+ this.state.options.chart.grid.horizontal.gap * 2 +
868
+ this.style['grid-line-horizontal']['stroke-width']
869
+ );
870
+ }
871
+ return this.state.options.row.height + this.state.options.chart.grid.horizontal.gap * 2;
872
+ },
873
+
874
+ /**
875
+ * Get specified tasks height
876
+ *
877
+ * @returns {number}
878
+ */
879
+ getTasksHeight(visibleTasks) {
880
+ return visibleTasks.length * this.getTaskHeight();
881
+ },
882
+
883
+ /**
884
+ * Convert time (in milliseconds) to pixel offset inside chart
885
+ *
886
+ * @param {int} ms
887
+ * @returns {number}
888
+ */
889
+ timeToPixelOffsetX(ms) {
890
+ let x = ms - this.state.options.times.firstTime;
891
+ if (x) {
892
+ x = x / this.state.options.times.timePerPixel;
893
+ }
894
+ return x;
895
+ },
896
+
897
+ /**
898
+ * Convert pixel offset inside chart to corresponding time offset in milliseconds
899
+ *
900
+ * @param {number} pixelOffsetX
901
+ * @returns {int} milliseconds
902
+ */
903
+ pixelOffsetXToTime(pixelOffsetX) {
904
+ let offset = pixelOffsetX + this.style['grid-line-vertical']['stroke-width'] / 2;
905
+ return offset * this.state.options.times.timePerPixel + this.state.options.times.firstTime;
906
+ },
907
+
908
+ /**
909
+ * Determine if element is inside current view port
910
+ *
911
+ * @param {number} x - element placement
912
+ * @param {number} width - element width
913
+ * @param {int} buffer - or threshold, if element is outside viewport but offset from view port is below this value return true
914
+ * @returns {boolean}
915
+ */
916
+ isInsideViewPort(x, width, buffer = 5000) {
917
+ return (
918
+ (x + width + buffer >= this.state.options.scroll.chart.left &&
919
+ x - buffer <= this.state.options.scroll.chart.right) ||
920
+ (x - buffer <= this.state.options.scroll.chart.left &&
921
+ x + width + buffer >= this.state.options.scroll.chart.right)
922
+ );
923
+ },
924
+
925
+ /**
926
+ * Chart scroll event handler
927
+ *
928
+ * @param {event} ev
929
+ */
930
+ onScrollChart(ev) {
931
+ this._onScrollChart(
932
+ this.state.refs.chartScrollContainerHorizontal.scrollLeft,
933
+ this.state.refs.chartScrollContainerVertical.scrollTop
934
+ );
935
+ },
936
+
937
+ /**
938
+ * After same as above but with different arguments - normalized
939
+ *
940
+ * @param {number} left
941
+ * @param {number} top
942
+ */
943
+ _onScrollChart(left, top) {
944
+ if (this.state.options.scroll.chart.left === left && this.state.options.scroll.chart.top === top) {
945
+ return;
946
+ }
947
+ const chartContainerWidth = this.state.refs.chartContainer.clientWidth;
948
+ this.state.options.scroll.chart.left = left;
949
+ this.state.options.scroll.chart.right = left + chartContainerWidth;
950
+ this.state.options.scroll.chart.percent = (left / this.state.options.times.totalViewDurationPx) * 100;
951
+ this.state.options.scroll.chart.top = top;
952
+ this.state.options.scroll.chart.time = this.pixelOffsetXToTime(left);
953
+ this.state.options.scroll.chart.timeCenter = this.pixelOffsetXToTime(left + chartContainerWidth / 2);
954
+ this.state.options.scroll.chart.dateTime.left = dayjs(this.state.options.scroll.chart.time).valueOf();
955
+ this.state.options.scroll.chart.dateTime.right = dayjs(
956
+ this.pixelOffsetXToTime(left + this.state.refs.chart.clientWidth)
957
+ ).valueOf();
958
+ this.scrollTo(left, top);
959
+ },
960
+
961
+ /**
962
+ * Scroll current chart to specified time (in milliseconds)
963
+ *
964
+ * @param {int} time
965
+ */
966
+ scrollToTime(time) {
967
+ let pos = this.timeToPixelOffsetX(time);
968
+ const chartContainerWidth = this.state.refs.chartContainer.clientWidth;
969
+ pos = pos - chartContainerWidth / 2;
970
+ if (pos > this.state.options.width) {
971
+ pos = this.state.options.width - chartContainerWidth;
972
+ }
973
+ this.scrollTo(pos);
974
+ },
975
+
976
+ /**
977
+ * Scroll chart or task list to specified pixel values
978
+ *
979
+ * @param {number|null} left
980
+ * @param {number|null} top
981
+ */
982
+ scrollTo(left = null, top = null) {
983
+ if (left !== null) {
984
+ this.state.refs.chartCalendarContainer.scrollLeft = left;
985
+ this.state.refs.chartGraphContainer.scrollLeft = left;
986
+ this.state.refs.chartScrollContainerHorizontal.scrollLeft = left;
987
+ this.state.options.scroll.left = left;
988
+ }
989
+ if (top !== null) {
990
+ this.state.refs.chartScrollContainerVertical.scrollTop = top;
991
+ this.state.refs.chartGraph.scrollTop = top;
992
+ this.state.refs.taskListItems.scrollTop = top;
993
+ this.state.options.scroll.top = top;
994
+ this.syncScrollTop();
995
+ }
996
+ },
997
+
998
+ /**
999
+ * After some actions like time zoom change we need to recompensate scroll position
1000
+ * so as a result everything will be in same place
1001
+ */
1002
+ fixScrollPos() {
1003
+ this.scrollToTime(this.state.options.scroll.chart.timeCenter);
1004
+ },
1005
+
1006
+ /**
1007
+ * Mouse wheel event handler
1008
+ */
1009
+ onWheelChart(ev) {
1010
+ if (!ev.shiftKey && ev.deltaX === 0) {
1011
+ let top = this.state.options.scroll.top + ev.deltaY;
1012
+ const chartClientHeight = this.state.options.rowsHeight;
1013
+ const scrollHeight = this.state.refs.chartGraph.scrollHeight - chartClientHeight;
1014
+ if (top < 0) {
1015
+ top = 0;
1016
+ } else if (top > scrollHeight) {
1017
+ top = scrollHeight;
1018
+ }
1019
+ this.scrollTo(null, top);
1020
+ } else if (ev.shiftKey && ev.deltaX === 0) {
1021
+ let left = this.state.options.scroll.left + ev.deltaY;
1022
+ const chartClientWidth = this.state.refs.chartScrollContainerHorizontal.clientWidth;
1023
+ const scrollWidth = this.state.refs.chartScrollContainerHorizontal.scrollWidth - chartClientWidth;
1024
+ if (left < 0) {
1025
+ left = 0;
1026
+ } else if (left > scrollWidth) {
1027
+ left = scrollWidth;
1028
+ }
1029
+ this.scrollTo(left);
1030
+ } else {
1031
+ let left = this.state.options.scroll.left + ev.deltaX;
1032
+ const chartClientWidth = this.state.refs.chartScrollContainerHorizontal.clientWidth;
1033
+ const scrollWidth = this.state.refs.chartScrollContainerHorizontal.scrollWidth - chartClientWidth;
1034
+ if (left < 0) {
1035
+ left = 0;
1036
+ } else if (left > scrollWidth) {
1037
+ left = scrollWidth;
1038
+ }
1039
+ this.scrollTo(left);
1040
+ }
1041
+ },
1042
+
1043
+ /**
1044
+ * Time zoom change event handler
1045
+ */
1046
+ onTimeZoomChange(timeZoom) {
1047
+ this.state.options.times.timeZoom = timeZoom;
1048
+ this.recalculateTimes();
1049
+ this.calculateSteps();
1050
+ this.fixScrollPos();
1051
+ },
1052
+
1053
+ /**
1054
+ * Row height change event handler
1055
+ */
1056
+ onRowHeightChange(height) {
1057
+ this.state.options.row.height = height;
1058
+ this.calculateTaskListColumnsDimensions();
1059
+ this.syncScrollTop();
1060
+ },
1061
+
1062
+ /**
1063
+ * Scope change event handler
1064
+ */
1065
+ onScopeChange(value) {
1066
+ this.state.options.scope.before = value;
1067
+ this.state.options.scope.after = value;
1068
+ this.initTimes();
1069
+ this.calculateSteps();
1070
+ this.computeCalendarWidths();
1071
+ this.fixScrollPos();
1072
+ },
1073
+
1074
+ /**
1075
+ * Task list width change event handler
1076
+ */
1077
+ onTaskListWidthChange(value) {
1078
+ this.state.options.taskList.percent = value;
1079
+ this.calculateTaskListColumnsDimensions();
1080
+ this.fixScrollPos();
1081
+ },
1082
+
1083
+ /**
1084
+ * Task list column width change event handler
1085
+ */
1086
+ onTaskListColumnWidthChange() {
1087
+ this.calculateTaskListColumnsDimensions();
1088
+ this.fixScrollPos();
1089
+ },
1090
+
1091
+ /**
1092
+ * Listen to specified event names
1093
+ */
1094
+ initializeEvents() {
1095
+ this.$on('chart-scroll-horizontal', this.onScrollChart);
1096
+ this.$on('chart-scroll-vertical', this.onScrollChart);
1097
+ this.$on('chart-wheel', this.onWheelChart);
1098
+ this.$on('times-timeZoom-change', this.onTimeZoomChange);
1099
+ this.$on('row-height-change', this.onRowHeightChange);
1100
+ this.$on('scope-change', this.onScopeChange);
1101
+ this.$on('taskList-width-change', this.onTaskListWidthChange);
1102
+ this.$on('taskList-column-width-change', this.onTaskListColumnWidthChange);
1103
+ },
1104
+
1105
+ /**
1106
+ * When some action was performed (scale change for example) - recalculate time variables
1107
+ */
1108
+ recalculateTimes() {
1109
+ let max = this.state.options.times.timeScale * 60;
1110
+ let min = this.state.options.times.timeScale;
1111
+ let steps = max / min;
1112
+ let percent = this.state.options.times.timeZoom / 100;
1113
+ this.state.options.times.timePerPixel =
1114
+ this.state.options.times.timeScale * steps * percent + Math.pow(2, this.state.options.times.timeZoom);
1115
+ this.state.options.times.totalViewDurationMs = dayjs(this.state.options.times.lastTime).diff(
1116
+ this.state.options.times.firstTime,
1117
+ 'milliseconds'
1118
+ );
1119
+ this.state.options.times.totalViewDurationPx =
1120
+ this.state.options.times.totalViewDurationMs / this.state.options.times.timePerPixel;
1121
+ this.state.options.width =
1122
+ this.state.options.times.totalViewDurationPx + this.style['grid-line-vertical']['stroke-width'];
1123
+ },
1124
+
1125
+ /**
1126
+ * Initialize time variables
1127
+ */
1128
+ initTimes() {
1129
+ this.state.options.times.firstTime = dayjs(this.state.options.times.firstTaskTime)
1130
+ .locale(this.state.options.locale.name)
1131
+ .startOf('day')
1132
+ .subtract(this.state.options.scope.before, 'days')
1133
+ .startOf('day')
1134
+ .valueOf();
1135
+ this.state.options.times.lastTime = dayjs(this.state.options.times.lastTaskTime)
1136
+ .locale(this.state.options.locale.name)
1137
+ .endOf('day')
1138
+ .add(this.state.options.scope.after, 'days')
1139
+ .endOf('day')
1140
+ .valueOf();
1141
+ this.recalculateTimes();
1142
+ },
1143
+
1144
+ /**
1145
+ * Calculate steps
1146
+ * Steps are days by default
1147
+ * Each step contain information about time offset and pixel offset of this time inside gantt chart
1148
+ */
1149
+ calculateSteps() {
1150
+ const steps = [];
1151
+ const lastMs = dayjs(this.state.options.times.lastTime).valueOf();
1152
+ const currentDate = dayjs(this.state.options.times.firstTime);
1153
+ steps.push({
1154
+ time: currentDate.valueOf(),
1155
+ offset: {
1156
+ ms: 0,
1157
+ px: 0
1158
+ }
1159
+ });
1160
+ for (
1161
+ let currentDate = dayjs(this.state.options.times.firstTime)
1162
+ .add(1, this.state.options.times.stepDuration)
1163
+ .startOf('day');
1164
+ currentDate.valueOf() <= lastMs;
1165
+ currentDate = currentDate.add(1, this.state.options.times.stepDuration).startOf('day')
1166
+ ) {
1167
+ const offsetMs = currentDate.diff(this.state.options.times.firstTime, 'milliseconds');
1168
+ const offsetPx = offsetMs / this.state.options.times.timePerPixel;
1169
+ const step = {
1170
+ time: currentDate.valueOf(),
1171
+ offset: {
1172
+ ms: offsetMs,
1173
+ px: offsetPx
1174
+ }
1175
+ };
1176
+ const previousStep = steps[steps.length - 1];
1177
+ previousStep.width = {
1178
+ ms: offsetMs - previousStep.offset.ms,
1179
+ px: offsetPx - previousStep.offset.px
1180
+ };
1181
+ steps.push(step);
1182
+ }
1183
+ const lastStep = steps[steps.length - 1];
1184
+ lastStep.width = {
1185
+ ms: this.state.options.times.totalViewDurationMs - lastStep.offset.ms,
1186
+ px: this.state.options.times.totalViewDurationPx - lastStep.offset.px
1187
+ };
1188
+ this.state.options.times.steps = steps;
1189
+ },
1190
+
1191
+ /**
1192
+ * Calculate calendar widths - when scale was changed for example
1193
+ */
1194
+ computeCalendarWidths() {
1195
+ this.computeDayWidths();
1196
+ this.computeHourWidths();
1197
+ this.computeMonthWidths();
1198
+ },
1199
+
1200
+ /**
1201
+ * Compute width of calendar hours column widths basing on text widths
1202
+ */
1203
+ computeHourWidths() {
1204
+ const style = { ...this.style['calendar-row-text'], ...this.style['calendar-row-text--hour'] };
1205
+ this.state.ctx.font = style['font-size'] + ' ' + style['font-family'];
1206
+ const localeName = this.state.options.locale.name;
1207
+ let currentDate = dayjs('2018-01-01T00:00:00').locale(localeName); // any date will be good for hours
1208
+ let maxWidths = this.state.options.calendar.hour.maxWidths;
1209
+ if (maxWidths.length) {
1210
+ return;
1211
+ }
1212
+ for (let formatName in this.state.options.calendar.hour.format) {
1213
+ maxWidths[formatName] = 0;
1214
+ }
1215
+ for (let hour = 0; hour < 24; hour++) {
1216
+ let widths = { hour };
1217
+ for (let formatName in this.state.options.calendar.hour.format) {
1218
+ const hourFormatted = this.state.options.calendar.hour.format[formatName](currentDate);
1219
+ this.state.options.calendar.hour.formatted[formatName].push(hourFormatted);
1220
+ widths[formatName] = this.state.ctx.measureText(hourFormatted).width;
1221
+ }
1222
+ this.state.options.calendar.hour.widths.push(widths);
1223
+ for (let formatName in this.state.options.calendar.hour.format) {
1224
+ if (widths[formatName] > maxWidths[formatName]) {
1225
+ maxWidths[formatName] = widths[formatName];
1226
+ }
1227
+ }
1228
+ currentDate = currentDate.add(1, 'hour');
1229
+ }
1230
+ },
1231
+
1232
+ /**
1233
+ * Compute calendar days column widths basing on text widths
1234
+ */
1235
+ computeDayWidths() {
1236
+ const style = { ...this.style['calendar-row-text'], ...this.style['calendar-row-text--day'] };
1237
+ this.state.ctx.font = style['font-size'] + ' ' + style['font-family'];
1238
+ const localeName = this.state.options.locale.name;
1239
+ let currentDate = dayjs(this.state.options.times.steps[0].time).locale(localeName);
1240
+ let maxWidths = this.state.options.calendar.day.maxWidths;
1241
+ this.state.options.calendar.day.widths = [];
1242
+ Object.keys(this.state.options.calendar.day.format).forEach(formatName => {
1243
+ maxWidths[formatName] = 0;
1244
+ });
1245
+ for (let day = 0, daysLen = this.state.options.times.steps.length; day < daysLen; day++) {
1246
+ const widths = {
1247
+ day
1248
+ };
1249
+ Object.keys(this.state.options.calendar.day.format).forEach(formatName => {
1250
+ widths[formatName] = this.state.ctx.measureText(
1251
+ this.state.options.calendar.day.format[formatName](currentDate)
1252
+ ).width;
1253
+ });
1254
+ this.state.options.calendar.day.widths.push(widths);
1255
+ Object.keys(this.state.options.calendar.day.format).forEach(formatName => {
1256
+ if (widths[formatName] > maxWidths[formatName]) {
1257
+ maxWidths[formatName] = widths[formatName];
1258
+ }
1259
+ });
1260
+ currentDate = currentDate.add(1, 'day');
1261
+ }
1262
+ },
1263
+
1264
+ /**
1265
+ * Months count
1266
+ *
1267
+ * @description Returns number of different months in specified time range
1268
+ *
1269
+ * @param {number} fromTime - date in ms
1270
+ * @param {number} toTime - date in ms
1271
+ *
1272
+ * @returns {number} different months count
1273
+ */
1274
+ monthsCount(fromTime, toTime) {
1275
+ if (fromTime > toTime) {
1276
+ return 0;
1277
+ }
1278
+ let currentMonth = dayjs(fromTime);
1279
+ let previousMonth = currentMonth.clone();
1280
+ let monthsCount = 1;
1281
+ while (currentMonth.valueOf() <= toTime) {
1282
+ currentMonth = currentMonth.add(1, 'day');
1283
+ if (previousMonth.month() !== currentMonth.month()) {
1284
+ monthsCount++;
1285
+ }
1286
+ previousMonth = currentMonth.clone();
1287
+ }
1288
+ return monthsCount;
1289
+ },
1290
+
1291
+ /**
1292
+ * Compute month calendar columns widths basing on text widths
1293
+ */
1294
+ computeMonthWidths() {
1295
+ const style = { ...this.style['calendar-row-text'], ...this.style['calendar-row-text--month'] };
1296
+ this.state.ctx.font = style['font-size'] + ' ' + style['font-family'];
1297
+ let maxWidths = this.state.options.calendar.month.maxWidths;
1298
+ this.state.options.calendar.month.widths = [];
1299
+ Object.keys(this.state.options.calendar.month.format).forEach(formatName => {
1300
+ maxWidths[formatName] = 0;
1301
+ });
1302
+ const localeName = this.state.options.locale.name;
1303
+ let currentDate = dayjs(this.state.options.times.firstTime).locale(localeName);
1304
+ const monthsCount = this.monthsCount(this.state.options.times.firstTime, this.state.options.times.lastTime);
1305
+ for (let month = 0; month < monthsCount; month++) {
1306
+ const widths = {
1307
+ month
1308
+ };
1309
+ Object.keys(this.state.options.calendar.month.format).forEach(formatName => {
1310
+ widths[formatName] = this.state.ctx.measureText(
1311
+ this.state.options.calendar.month.format[formatName](currentDate)
1312
+ ).width;
1313
+ });
1314
+ this.state.options.calendar.month.widths.push(widths);
1315
+ Object.keys(this.state.options.calendar.month.format).forEach(formatName => {
1316
+ if (widths[formatName] > maxWidths[formatName]) {
1317
+ maxWidths[formatName] = widths[formatName];
1318
+ }
1319
+ });
1320
+ currentDate = currentDate.add(1, 'month');
1321
+ }
1322
+ },
1323
+
1324
+ /**
1325
+ * Prepare time and date variables for gantt
1326
+ */
1327
+ prepareDates() {
1328
+ let firstTaskTime = Number.MAX_SAFE_INTEGER;
1329
+ let lastTaskTime = 0;
1330
+ for (let index = 0, len = this.state.tasks.length; index < len; index++) {
1331
+ let task = this.state.tasks[index];
1332
+ if (task.startTime < firstTaskTime) {
1333
+ firstTaskTime = task.startTime;
1334
+ }
1335
+ if (task.startTime + task.duration > lastTaskTime) {
1336
+ lastTaskTime = task.startTime + task.duration;
1337
+ }
1338
+ }
1339
+ this.state.options.times.firstTaskTime = firstTaskTime;
1340
+ this.state.options.times.lastTaskTime = lastTaskTime;
1341
+ this.state.options.times.firstTime = dayjs(firstTaskTime)
1342
+ .locale(this.state.options.locale.name)
1343
+ .startOf('day')
1344
+ .subtract(this.state.options.scope.before, 'days')
1345
+ .startOf('day')
1346
+ .valueOf();
1347
+ this.state.options.times.lastTime = dayjs(lastTaskTime)
1348
+ .locale(this.state.options.locale.name)
1349
+ .endOf('day')
1350
+ .add(this.state.options.scope.after, 'days')
1351
+ .endOf('day')
1352
+ .valueOf();
1353
+ },
1354
+
1355
+ /**
1356
+ * Setup and calculate everything
1357
+ */
1358
+ setup(itsUpdate = '') {
1359
+ this.initialize(itsUpdate);
1360
+ this.prepareDates();
1361
+ this.initTimes();
1362
+ this.calculateSteps();
1363
+ this.computeCalendarWidths();
1364
+ this.state.options.taskList.width = this.state.options.taskList.columns.reduce(
1365
+ (prev, current) => {
1366
+ return { width: prev.width + current.width };
1367
+ },
1368
+ { width: 0 }
1369
+ ).width;
1370
+ },
1371
+
1372
+ /**
1373
+ * Global resize event (from window.addEventListener)
1374
+ */
1375
+ globalOnResize() {
1376
+ if (typeof this.$el === 'undefined' || !this.$el) {
1377
+ return;
1378
+ }
1379
+ this.state.options.clientWidth = this.$el.clientWidth;
1380
+ if (
1381
+ this.state.options.taskList.widthFromPercentage >
1382
+ (this.state.options.clientWidth / 100) * this.state.options.taskList.widthThreshold
1383
+ ) {
1384
+ const diff =
1385
+ this.state.options.taskList.widthFromPercentage -
1386
+ (this.state.options.clientWidth / 100) * this.state.options.taskList.widthThreshold;
1387
+ let diffPercent = 100 - (diff / this.state.options.taskList.widthFromPercentage) * 100;
1388
+ if (diffPercent < 0) {
1389
+ diffPercent = 0;
1390
+ }
1391
+ this.state.options.taskList.columns.forEach(column => {
1392
+ column.thresholdPercent = diffPercent;
1393
+ });
1394
+ } else {
1395
+ this.state.options.taskList.columns.forEach(column => {
1396
+ column.thresholdPercent = 100;
1397
+ });
1398
+ }
1399
+ this.calculateTaskListColumnsDimensions();
1400
+ this.$emit('calendar-recalculate');
1401
+ this.syncScrollTop();
1402
+ }
1403
+ },
1404
+
1405
+ computed: {
1406
+ /**
1407
+ * Get visible tasks
1408
+ * Very important method which will bring us only those tasks that are visible inside gantt chart
1409
+ * For example when task is collapsed - children of this task are not visible - we should not render them
1410
+ */
1411
+ visibleTasks() {
1412
+ const visibleTasks = this.state.tasks.filter(task => this.isTaskVisible(task));
1413
+ const maxRows = visibleTasks.slice(0, this.state.options.maxRows);
1414
+ this.state.options.rowsHeight = this.getTasksHeight(maxRows);
1415
+ let heightCompensation = 0;
1416
+ if (this.state.options.maxHeight && this.state.options.rowsHeight > this.state.options.maxHeight) {
1417
+ heightCompensation = this.state.options.rowsHeight - this.state.options.maxHeight;
1418
+ this.state.options.rowsHeight = this.state.options.maxHeight;
1419
+ }
1420
+ this.state.options.height = this.getHeight(maxRows) - heightCompensation;
1421
+ this.state.options.allVisibleTasksHeight = this.getTasksHeight(visibleTasks);
1422
+ this.state.options.outerHeight = this.getHeight(maxRows, true) - heightCompensation;
1423
+ let len = visibleTasks.length;
1424
+ for (let index = 0; index < len; index++) {
1425
+ let task = visibleTasks[index];
1426
+ task.width =
1427
+ task.duration / this.state.options.times.timePerPixel - this.style['grid-line-vertical']['stroke-width'];
1428
+ if (task.width < 0) {
1429
+ task.width = 0;
1430
+ }
1431
+ task.height = this.state.options.row.height;
1432
+ task.x = this.timeToPixelOffsetX(task.startTime);
1433
+ task.y =
1434
+ (this.state.options.row.height + this.state.options.chart.grid.horizontal.gap * 2) * index +
1435
+ this.state.options.chart.grid.horizontal.gap;
1436
+ }
1437
+ return visibleTasks;
1438
+ },
1439
+
1440
+ /**
1441
+ * Style shortcut
1442
+ */
1443
+ style() {
1444
+ return this.state.dynamicStyle;
1445
+ },
1446
+
1447
+ /**
1448
+ * Get columns and compute dimensions on the fly
1449
+ */
1450
+ getTaskListColumns() {
1451
+ this.calculateTaskListColumnsDimensions();
1452
+ return this.state.options.taskList.columns;
1453
+ },
1454
+
1455
+ /**
1456
+ * Tasks used for communicate with parent component
1457
+ */
1458
+ outputTasks() {
1459
+ return this.state.tasks;
1460
+ },
1461
+
1462
+ /**
1463
+ * Options used to communicate with parent component
1464
+ */
1465
+ outputOptions() {
1466
+ return this.state.options;
1467
+ }
1468
+ },
1469
+
1470
+ /**
1471
+ * Watch tasks after gantt instance is created and react when we have new kids on the block
1472
+ */
1473
+ created() {
1474
+ this.initializeEvents();
1475
+ this.setup();
1476
+ this.state.unwatchTasks = this.$watch(
1477
+ 'tasks',
1478
+ tasks => {
1479
+ const notEqual = notEqualDeep(tasks, this.outputTasks);
1480
+ if (notEqual) {
1481
+ this.setup('tasks');
1482
+ }
1483
+ },
1484
+ { deep: true }
1485
+ );
1486
+ this.state.unwatchOptions = this.$watch(
1487
+ 'options',
1488
+ opts => {
1489
+ const notEqual = notEqualDeep(opts, this.outputOptions);
1490
+ if (notEqual) {
1491
+ this.setup('options');
1492
+ }
1493
+ },
1494
+ { deep: true }
1495
+ );
1496
+ this.state.unwatchStyle = this.$watch(
1497
+ 'dynamicStyle',
1498
+ style => {
1499
+ const notEqual = notEqualDeep(style, this.dynamicStyle);
1500
+ if (notEqual) {
1501
+ this.initializeStyle();
1502
+ }
1503
+ },
1504
+ { deep: true, immediate: true }
1505
+ );
1506
+
1507
+ this.state.unwatchOutputTasks = this.$watch(
1508
+ 'outputTasks',
1509
+ tasks => {
1510
+ this.$emit('tasks-changed', tasks.map(task => task));
1511
+ },
1512
+ { deep: true }
1513
+ );
1514
+ this.state.unwatchOutputOptions = this.$watch(
1515
+ 'outputOptions',
1516
+ options => {
1517
+ this.$emit('options-changed', mergeDeep({}, options));
1518
+ },
1519
+ { deep: true }
1520
+ );
1521
+ this.state.unwatchOutputStyle = this.$watch(
1522
+ 'style',
1523
+ style => {
1524
+ this.$emit('dynamic-style-changed', mergeDeep({}, style));
1525
+ },
1526
+ { deep: true }
1527
+ );
1528
+
1529
+ this.$root.$emit('gantt-elastic-created', this);
1530
+ this.$emit('created', this);
1531
+ },
1532
+
1533
+ /**
1534
+ * Emit before-mount event
1535
+ */
1536
+ beforeMount() {
1537
+ this.$emit('before-mount', this);
1538
+ },
1539
+
1540
+ /**
1541
+ * Emit ready/mounted events and deliver this gantt instance to outside world when needed
1542
+ */
1543
+ mounted() {
1544
+ this.state.options.clientWidth = this.$el.clientWidth;
1545
+ this.state.resizeObserver = new ResizeObserver((entries, observer) => {
1546
+ this.globalOnResize();
1547
+ });
1548
+ this.state.resizeObserver.observe(this.$el.parentNode);
1549
+ this.globalOnResize();
1550
+ this.$emit('ready', this);
1551
+ this.$root.$emit('gantt-elastic-mounted', this);
1552
+ this.$emit('mounted', this);
1553
+ this.$root.$emit('gantt-elastic-ready', this);
1554
+ },
1555
+
1556
+ /**
1557
+ * Emit event when data was changed and before update (you can cleanup dom events here for example)
1558
+ */
1559
+ beforeUpdate() {
1560
+ this.$emit('before-update');
1561
+ },
1562
+
1563
+ /**
1564
+ * Emit event when gantt-elastic view was updated
1565
+ */
1566
+ updated() {
1567
+ this.$nextTick(() => {
1568
+ this.$emit('updated');
1569
+ });
1570
+ },
1571
+
1572
+ /**
1573
+ * Before destroy event - clean up
1574
+ */
1575
+ beforeDestroy() {
1576
+ this.state.resizeObserver.unobserve(this.$el.parentNode);
1577
+ this.state.unwatchTasks();
1578
+ this.state.unwatchOptions();
1579
+ this.state.unwatchStyle();
1580
+ this.state.unwatchOutputTasks();
1581
+ this.state.unwatchOutputOptions();
1582
+ this.state.unwatchOutputStyle();
1583
+ this.$emit('before-destroy');
1584
+ },
1585
+
1586
+ /**
1587
+ * Emit event after gantt-elastic was destroyed
1588
+ */
1589
+ destroyed() {
1590
+ this.$emit('destroyed');
1591
+ }
1592
+ };
1593
+ export default GanttElastic;
1594
+ </script>
1595
+
1596
+ <style>
1597
+ [class^='gantt-elastic'],
1598
+ [class*=' gantt-elastic'] {
1599
+ box-sizing: border-box;
1600
+ }
1601
+
1602
+ .gantt-elastic__main-view svg {
1603
+ display: block;
1604
+ }
1605
+
1606
+ .gantt-elastic__grid-horizontal-line,
1607
+ .gantt-elastic__grid-vertical-line {
1608
+ stroke: #a0a0a0;
1609
+ stroke-width: 1;
1610
+ }
1611
+
1612
+ foreignObject>* {
1613
+ margin: 0px;
1614
+ }
1615
+
1616
+ .gantt-elastic .p-2 {
1617
+ padding: 10rem;
1618
+ }
1619
+
1620
+ .gantt-elastic__main-view-main-container,
1621
+ .gantt-elastic__main-view-container {
1622
+ overflow: hidden;
1623
+ max-width: 100%;
1624
+ }
1625
+
1626
+ .gantt-elastic__task-list-header-column:last-of-type {
1627
+ border-right: 1px solid #00000050;
1628
+ }
1629
+
1630
+ .gantt-elastic__task-list-item:last-of-type {
1631
+ border-bottom: 1px solid #00000050;
1632
+ }
1633
+
1634
+ .gantt-elastic__task-list-item-value-wrapper:hover {
1635
+ overflow: visible !important;
1636
+ }
1637
+
1638
+ .gantt-elastic__task-list-item-value-wrapper:hover>.gantt-elastic__task-list-item-value-container {
1639
+ position: relative;
1640
+ overflow: visible !important;
1641
+ }
1642
+
1643
+ .gantt-elastic__task-list-item-value-wrapper:hover>.gantt-elastic__task-list-item-value {
1644
+ position: absolute;
1645
+ }
1646
+ </style>