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.
- package/babel.config.js +5 -0
- package/package.json +1 -4
- package/src/.eslintrc.js +18 -0
- package/src/App.vue +780 -0
- package/src/GanttElastic.standalone.vue +48 -0
- package/src/GanttElastic.vue +2305 -0
- package/src/assets/logo.png +0 -0
- package/src/components/Calendar/Calendar.vue +559 -0
- package/src/components/Calendar/CalendarRow.vue +112 -0
- package/src/components/Chart/Chart.vue +117 -0
- package/src/components/Chart/DaysHighlight.vue +60 -0
- package/src/components/Chart/DependencyLines.vue +112 -0
- package/src/components/Chart/Grid.vue +205 -0
- package/src/components/Chart/ProgressBar.vue +110 -0
- package/src/components/Chart/Row/Epic.vue +131 -0
- package/src/components/Chart/Row/Milestone.vue +117 -0
- package/src/components/Chart/Row/Project.vue +132 -0
- package/src/components/Chart/Row/Story.vue +127 -0
- package/src/components/Chart/Row/Subtask.vue +117 -0
- package/src/components/Chart/Row/Task.mixin.js +47 -0
- package/src/components/Chart/Row/Task.vue +82 -0
- package/src/components/Chart/Text.vue +105 -0
- package/src/components/Expander.vue +114 -0
- package/src/components/GanttElastic.standalone.vue +48 -0
- package/src/components/GanttElastic.vue +1646 -0
- package/src/components/Header/GanttViewFilter.vue +154 -0
- package/src/components/Header/Header.vue +266 -0
- package/src/components/MainView.vue +283 -0
- package/src/components/TaskList/ItemColumn.vue +212 -0
- package/src/components/TaskList/TaskList.vue +45 -0
- package/src/components/TaskList/TaskListHeader.vue +143 -0
- package/src/components/TaskList/TaskListItem.vue +35 -0
- package/src/components/bundle.js +28 -0
- package/src/components/components/Calendar/Calendar.vue +332 -0
- package/src/components/components/Calendar/CalendarRow.vue +96 -0
- package/src/components/components/Chart/Chart.vue +111 -0
- package/src/components/components/Chart/DaysHighlight.vue +71 -0
- package/src/components/components/Chart/DependencyLines.vue +112 -0
- package/src/components/components/Chart/Grid.vue +164 -0
- package/src/components/components/Chart/ProgressBar.vue +110 -0
- package/src/components/components/Chart/Row/Milestone.vue +117 -0
- package/src/components/components/Chart/Row/Project.vue +131 -0
- package/src/components/components/Chart/Row/Task.mixin.js +46 -0
- package/src/components/components/Chart/Row/Task.vue +107 -0
- package/src/components/components/Chart/Text.vue +105 -0
- package/src/components/components/Expander.vue +126 -0
- package/src/components/components/Header/Header.vue +265 -0
- package/src/components/components/MainView.vue +282 -0
- package/src/components/components/TaskList/ItemColumn.vue +121 -0
- package/src/components/components/TaskList/TaskList.vue +45 -0
- package/src/components/components/TaskList/TaskListHeader.vue +143 -0
- package/src/components/components/TaskList/TaskListItem.vue +35 -0
- package/src/components/components/bundle.js +28 -0
- package/src/components/style.js +308 -0
- package/src/index.js +12 -0
- package/src/main.js +6 -0
- package/src/style.js +398 -0
- package/vue.config.js +42 -0
- package/dist/demo.html +0 -1
- package/dist/tgganttchart.common.js +0 -9232
- package/dist/tgganttchart.common.js.map +0 -1
- package/dist/tgganttchart.css +0 -1
- package/dist/tgganttchart.umd.js +0 -9243
- package/dist/tgganttchart.umd.js.map +0 -1
- package/dist/tgganttchart.umd.min.js +0 -7
- 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>
|