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,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>
|