@fatcore/gantt-lite 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +16 -0
- package/.vscode/extensions.json +4 -0
- package/.vscode/launch.json +20 -0
- package/.vscode/tasks.json +42 -0
- package/README.md +27 -0
- package/angular.json +165 -0
- package/dist17/gantt-lite/README.md +24 -0
- package/dist17/gantt-lite/esm2022/gantt-lite.mjs +5 -0
- package/dist17/gantt-lite/esm2022/gantt-lite.module.mjs +18 -0
- package/dist17/gantt-lite/esm2022/lib/gantt-lite-base.mjs +290 -0
- package/dist17/gantt-lite/esm2022/lib/gantt-lite.component.mjs +359 -0
- package/dist17/gantt-lite/esm2022/lib/gantt-lite.helper.mjs +107 -0
- package/dist17/gantt-lite/esm2022/lib/gantt-lite.model.mjs +2 -0
- package/dist17/gantt-lite/esm2022/public-api.mjs +3 -0
- package/dist17/gantt-lite/fesm2022/gantt-lite.mjs +774 -0
- package/dist17/gantt-lite/fesm2022/gantt-lite.mjs.map +1 -0
- package/dist17/gantt-lite/gantt-lite.module.d.ts +8 -0
- package/dist17/gantt-lite/index.d.ts +5 -0
- package/dist17/gantt-lite/lib/gantt-lite-base.d.ts +50 -0
- package/dist17/gantt-lite/lib/gantt-lite.component.d.ts +55 -0
- package/dist17/gantt-lite/lib/gantt-lite.helper.d.ts +20 -0
- package/dist17/gantt-lite/lib/gantt-lite.model.d.ts +47 -0
- package/dist17/gantt-lite/public-api.d.ts +2 -0
- package/dist18/gantt-lite/README.md +24 -0
- package/dist18/gantt-lite/esm2022/gantt-lite.mjs +5 -0
- package/dist18/gantt-lite/esm2022/gantt-lite.module.mjs +18 -0
- package/dist18/gantt-lite/esm2022/lib/gantt-lite-base.mjs +290 -0
- package/dist18/gantt-lite/esm2022/lib/gantt-lite.component.mjs +359 -0
- package/dist18/gantt-lite/esm2022/lib/gantt-lite.helper.mjs +107 -0
- package/dist18/gantt-lite/esm2022/lib/gantt-lite.model.mjs +2 -0
- package/dist18/gantt-lite/esm2022/public-api.mjs +3 -0
- package/dist18/gantt-lite/fesm2022/gantt-lite.mjs +774 -0
- package/dist18/gantt-lite/fesm2022/gantt-lite.mjs.map +1 -0
- package/dist18/gantt-lite/gantt-lite.module.d.ts +8 -0
- package/dist18/gantt-lite/index.d.ts +5 -0
- package/dist18/gantt-lite/lib/gantt-lite-base.d.ts +50 -0
- package/dist18/gantt-lite/lib/gantt-lite.component.d.ts +55 -0
- package/dist18/gantt-lite/lib/gantt-lite.helper.d.ts +20 -0
- package/dist18/gantt-lite/lib/gantt-lite.model.d.ts +47 -0
- package/dist18/gantt-lite/public-api.d.ts +2 -0
- package/dist19/gantt-lite/README.md +24 -0
- package/dist19/gantt-lite/fesm2022/gantt-lite.mjs +774 -0
- package/dist19/gantt-lite/fesm2022/gantt-lite.mjs.map +1 -0
- package/dist19/gantt-lite/gantt-lite.module.d.ts +8 -0
- package/dist19/gantt-lite/index.d.ts +5 -0
- package/dist19/gantt-lite/lib/gantt-lite-base.d.ts +50 -0
- package/dist19/gantt-lite/lib/gantt-lite.component.d.ts +55 -0
- package/dist19/gantt-lite/lib/gantt-lite.helper.d.ts +20 -0
- package/dist19/gantt-lite/lib/gantt-lite.model.d.ts +47 -0
- package/dist19/gantt-lite/public-api.d.ts +2 -0
- package/dist20/gantt-lite/README.md +24 -0
- package/dist20/gantt-lite/fesm2022/gantt-lite.mjs +774 -0
- package/dist20/gantt-lite/fesm2022/gantt-lite.mjs.map +1 -0
- package/dist20/gantt-lite/index.d.ts +159 -0
- package/my-workspace-0.0.0.tgz +0 -0
- package/package.json +45 -0
- package/projects/gantt-lite/README.md +24 -0
- package/projects/gantt-lite/ng-package.json +7 -0
- package/projects/gantt-lite/package.json +10 -0
- package/projects/gantt-lite/src/gantt-lite.module.ts +10 -0
- package/projects/gantt-lite/src/lib/gantt-lite-base.ts +300 -0
- package/projects/gantt-lite/src/lib/gantt-lite.component.html +128 -0
- package/projects/gantt-lite/src/lib/gantt-lite.component.scss +323 -0
- package/projects/gantt-lite/src/lib/gantt-lite.component.ts +391 -0
- package/projects/gantt-lite/src/lib/gantt-lite.helper.ts +124 -0
- package/projects/gantt-lite/src/lib/gantt-lite.model.ts +56 -0
- package/projects/gantt-lite/src/public-api.ts +5 -0
- package/projects/gantt-lite/tsconfig.lib.json +14 -0
- package/projects/gantt-lite/tsconfig.lib.prod.json +10 -0
- package/projects/gantt-lite/tsconfig.spec.json +14 -0
- package/projects/my-app/server.ts +56 -0
- package/projects/my-app/src/app/app.component.html +336 -0
- package/projects/my-app/src/app/app.component.scss +0 -0
- package/projects/my-app/src/app/app.component.spec.ts +29 -0
- package/projects/my-app/src/app/app.component.ts +13 -0
- package/projects/my-app/src/app/app.config.server.ts +11 -0
- package/projects/my-app/src/app/app.config.ts +9 -0
- package/projects/my-app/src/app/app.routes.ts +3 -0
- package/projects/my-app/src/assets/.gitkeep +0 -0
- package/projects/my-app/src/favicon.ico +0 -0
- package/projects/my-app/src/index.html +13 -0
- package/projects/my-app/src/main.server.ts +7 -0
- package/projects/my-app/src/main.ts +6 -0
- package/projects/my-app/src/styles.scss +1 -0
- package/projects/my-app/tsconfig.app.json +18 -0
- package/projects/my-app/tsconfig.spec.json +14 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/* GANTT ROOT */
|
|
2
|
+
:host {
|
|
3
|
+
display: block;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.gantt-root {
|
|
9
|
+
height: 100%;
|
|
10
|
+
width: 100%;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.gantt-body {
|
|
17
|
+
flex: 1;
|
|
18
|
+
display: flex;
|
|
19
|
+
overflow-y: auto;
|
|
20
|
+
overflow-x: hidden;
|
|
21
|
+
border-width: 1px;
|
|
22
|
+
border-style: solid;
|
|
23
|
+
border-color: rgba(170, 170, 170, 0.8);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* TOOLBAR */
|
|
27
|
+
.toolbar {
|
|
28
|
+
flex-shrink: 0;
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 8px;
|
|
31
|
+
background: white;
|
|
32
|
+
position: sticky;
|
|
33
|
+
top: 0;
|
|
34
|
+
z-index: 50;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.toolbar button.active {
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* LOADING */
|
|
42
|
+
.loading-overlay {
|
|
43
|
+
position: sticky;
|
|
44
|
+
inset: 0;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
z-index: 1000;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* TOOLTIP */
|
|
52
|
+
.global-tooltip {
|
|
53
|
+
position: fixed;
|
|
54
|
+
background: rgba(88, 88, 88, 0.9);
|
|
55
|
+
color: white;
|
|
56
|
+
padding: 8px;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
font-size: 12px;
|
|
59
|
+
z-index: 1000;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* LEFT PANEL */
|
|
64
|
+
.left-panel {
|
|
65
|
+
flex-shrink: 0;
|
|
66
|
+
background: white;
|
|
67
|
+
border-right: 1px solid #dcdcdc;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.task-row {
|
|
71
|
+
display: grid;
|
|
72
|
+
grid-auto-flow: column;
|
|
73
|
+
grid-auto-columns: 120px;
|
|
74
|
+
gap: 0 10px;
|
|
75
|
+
font-size: small;
|
|
76
|
+
align-items: center;
|
|
77
|
+
border-bottom: 1px solid #eee;
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.cell {
|
|
83
|
+
padding: 0 8px;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
text-overflow: ellipsis;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.task-row.header {
|
|
90
|
+
font-size: small;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
position: sticky;
|
|
93
|
+
top: 0;
|
|
94
|
+
background: white !important;
|
|
95
|
+
z-index: 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.task-row:hover {
|
|
99
|
+
background: rgba(255, 0, 0, 0.1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.highlighted-row {
|
|
103
|
+
background: rgba(255, 0, 0, 0.3) !important;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* RESIZER */
|
|
107
|
+
.resizer {
|
|
108
|
+
min-width: 7px;
|
|
109
|
+
cursor: col-resize;
|
|
110
|
+
background: #b4b4b4;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.resizer:hover {
|
|
114
|
+
background: #696969;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
/* RIGHT PANEL */
|
|
119
|
+
.right-panel {
|
|
120
|
+
flex: 1;
|
|
121
|
+
min-width: 100px !important;
|
|
122
|
+
position: relative;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.right-panel-dead {
|
|
126
|
+
flex: 1;
|
|
127
|
+
height: 10000px;
|
|
128
|
+
min-width: 100px !important;
|
|
129
|
+
position: relative;
|
|
130
|
+
background-color: #696969;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ONLY HORIZONTAL SCROLL */
|
|
134
|
+
.horizontal-scroll {
|
|
135
|
+
overflow-x: hidden;
|
|
136
|
+
overflow-y: hidden;
|
|
137
|
+
position: relative;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* MAIN CONTENT */
|
|
141
|
+
.gantt-content {
|
|
142
|
+
position: relative;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* TIMELINE HEADER */
|
|
146
|
+
.timeline-header {
|
|
147
|
+
display: flex;
|
|
148
|
+
position: sticky;
|
|
149
|
+
top: 0;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
white-space: nowrap;
|
|
152
|
+
z-index: 20;
|
|
153
|
+
background: white;
|
|
154
|
+
border-bottom: 1px solid #ccc;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.timeline-slot {
|
|
158
|
+
flex: 0 0 auto;
|
|
159
|
+
border-right: 1px solid #e0e0e0;
|
|
160
|
+
padding: 8px;
|
|
161
|
+
box-sizing: border-box;
|
|
162
|
+
text-align: center;
|
|
163
|
+
font-size: 12px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.highlighted-task {
|
|
167
|
+
background: rgba(255, 0, 0, 0.4) !important;
|
|
168
|
+
border-color: #b71c1c !important;
|
|
169
|
+
z-index: 10;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* GRID */
|
|
173
|
+
.grid-layer {
|
|
174
|
+
position: absolute;
|
|
175
|
+
inset: 0;
|
|
176
|
+
pointer-events: none;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.grid-line {
|
|
180
|
+
position: absolute;
|
|
181
|
+
top: 0;
|
|
182
|
+
bottom: 0;
|
|
183
|
+
width: 1px;
|
|
184
|
+
background: rgba(200, 200, 200, 0.35);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.horizontal-grid-layer {
|
|
188
|
+
position: absolute;
|
|
189
|
+
inset: 0;
|
|
190
|
+
pointer-events: none;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.horizontal-line {
|
|
194
|
+
position: absolute;
|
|
195
|
+
left: 0;
|
|
196
|
+
right: 0;
|
|
197
|
+
height: 1px;
|
|
198
|
+
background: rgba(200, 200, 200, 0.25);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* DEPENDENCIES */
|
|
202
|
+
.dependencies-svg {
|
|
203
|
+
overflow: visible;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.dependencies-normal {
|
|
207
|
+
pointer-events: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.dependencies-highlighted {
|
|
211
|
+
pointer-events: none;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* NORMAL DEPENDENCIES */
|
|
215
|
+
.dependency {
|
|
216
|
+
stroke: #9ba2ad;
|
|
217
|
+
stroke-width: 1.6;
|
|
218
|
+
fill: none;
|
|
219
|
+
opacity: 0.35;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* HIGHLIGHTED DEPENDENCIES */
|
|
223
|
+
.highlighted-dependency {
|
|
224
|
+
stroke: #52555a;
|
|
225
|
+
stroke-width: 2.7;
|
|
226
|
+
fill: none;
|
|
227
|
+
opacity: 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* OPTIONAL: fade non-selected dependencies when hovering */
|
|
231
|
+
.dependencies-svg.hovering .dependency {
|
|
232
|
+
opacity: 0.035;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* TASKS */
|
|
236
|
+
.tasks-layer {
|
|
237
|
+
position: relative;
|
|
238
|
+
z-index: 2;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.task {
|
|
242
|
+
position: absolute;
|
|
243
|
+
background: rgba(155, 148, 247, 0.75);
|
|
244
|
+
opacity: 0.85;
|
|
245
|
+
color: rgb(80, 80, 80);
|
|
246
|
+
padding-top: 4px;
|
|
247
|
+
border-radius: 4px;
|
|
248
|
+
font-size: 11px;
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
min-width: 2px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.task:hover {
|
|
254
|
+
background: rgba(255, 0, 0, 0.15);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* SPINNER */
|
|
258
|
+
.spinner {
|
|
259
|
+
width: 40px;
|
|
260
|
+
height: 40px;
|
|
261
|
+
border: 4px solid #ddd;
|
|
262
|
+
border-top-color: #3f51b5;
|
|
263
|
+
border-radius: 50%;
|
|
264
|
+
animation: spin 1s linear infinite;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@keyframes spin {
|
|
268
|
+
to {
|
|
269
|
+
transform: rotate(360deg);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
* {
|
|
274
|
+
box-sizing: border-box;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* BUTTON SCROLL GANTT */
|
|
278
|
+
.floating-nav {
|
|
279
|
+
position: sticky;
|
|
280
|
+
top: 50%;
|
|
281
|
+
transform: translateY(-50%);
|
|
282
|
+
z-index: 100;
|
|
283
|
+
pointer-events: none;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.nav-btn {
|
|
287
|
+
position: absolute;
|
|
288
|
+
width: 45px;
|
|
289
|
+
height: 45px;
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
justify-content: center;
|
|
293
|
+
pointer-events: auto;
|
|
294
|
+
background: rgba(92, 154, 248, 0.4);
|
|
295
|
+
border: none;
|
|
296
|
+
color: rgb(83, 83, 83);
|
|
297
|
+
font-size: 26px;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
backdrop-filter: blur(1px);
|
|
300
|
+
border-radius: 2px;
|
|
301
|
+
line-height: 1;
|
|
302
|
+
text-align: center;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.nav-btn:hover {
|
|
306
|
+
background: rgba(92, 154, 248, 0.7);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.nav-btn.left {
|
|
310
|
+
margin-left: 13px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.nav-btn.right {
|
|
314
|
+
right: 23px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.task-label {
|
|
318
|
+
padding-left: 6px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.hidden {
|
|
322
|
+
display: none;
|
|
323
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { Component, ElementRef, ViewChild, Input, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
|
2
|
+
import { DependencyPath, GanttScale, GanttTask, GanttTaskView } from './gantt-lite.model';
|
|
3
|
+
import { SlotIndexToDateLabelConverter } from './gantt-lite.helper';
|
|
4
|
+
import { GanttLiteBase } from './gantt-lite-base';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'gantt-lite',
|
|
8
|
+
templateUrl: './gantt-lite.component.html',
|
|
9
|
+
styleUrls: ['./gantt-lite.component.scss'],
|
|
10
|
+
standalone: false,
|
|
11
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
12
|
+
})
|
|
13
|
+
export class GanttLiteComponent extends GanttLiteBase {
|
|
14
|
+
@Input()
|
|
15
|
+
public set rawTasks(value: GanttTask[]) {
|
|
16
|
+
this._rawTasks = value ?? [];
|
|
17
|
+
this.recalculteAll();
|
|
18
|
+
}
|
|
19
|
+
public get rawTasks(): GanttTask[] {
|
|
20
|
+
return this._rawTasks;
|
|
21
|
+
}
|
|
22
|
+
@ViewChild('ganttBodyScroll') private ganttBodyScroll!: ElementRef<HTMLDivElement>;
|
|
23
|
+
@ViewChild('ganttContent') private ganttContent!: ElementRef<HTMLDivElement>;
|
|
24
|
+
@ViewChild('ganttScroll') private ganttScroll!: ElementRef<HTMLDivElement>;
|
|
25
|
+
@ViewChild('leftPanel') private leftPanel!: ElementRef<HTMLButtonElement>;
|
|
26
|
+
@ViewChild('timelineHeader') private timelineHeader!: ElementRef<HTMLDivElement>;
|
|
27
|
+
@ViewChild('leftBtn') private leftBtn!: ElementRef<HTMLButtonElement>;
|
|
28
|
+
@ViewChild('rightBtn') private rightBtn!: ElementRef<HTMLButtonElement>;
|
|
29
|
+
public toolbarHeight: number = 27;
|
|
30
|
+
public hoveredDependencyIds = new Set<string>();
|
|
31
|
+
public selectedTaskId: string | null = null;
|
|
32
|
+
public tooltipTask: GanttTask | null = null;
|
|
33
|
+
public tooltipX = 0;
|
|
34
|
+
public tooltipY = 0;
|
|
35
|
+
public maxSlotsAlert: boolean = false;
|
|
36
|
+
public loading: boolean = false;
|
|
37
|
+
public showLeftBtn = false;
|
|
38
|
+
public showRightBtn = true;
|
|
39
|
+
public buttonsHeight: number = 20;
|
|
40
|
+
public viewTasks: GanttTaskView[] = [];
|
|
41
|
+
private _rawTasks: GanttTask[] = [];
|
|
42
|
+
private originDate: number = 0;
|
|
43
|
+
private resizing = false;
|
|
44
|
+
private scrollInterval: any;
|
|
45
|
+
private labelConverter: SlotIndexToDateLabelConverter | null = null;
|
|
46
|
+
|
|
47
|
+
constructor(private cdr: ChangeDetectorRef) { super(); }
|
|
48
|
+
|
|
49
|
+
// recalculate all Gantt
|
|
50
|
+
private recalculteAll(): void {
|
|
51
|
+
this.loading = true;
|
|
52
|
+
this.cdr.markForCheck();
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
const start = performance.now();
|
|
55
|
+
this.refreshVisibleColumns();
|
|
56
|
+
this.scaleSelected = this.getScaleConfig(this.defaultScale);
|
|
57
|
+
|
|
58
|
+
if (this.dataType !== 'duration') {
|
|
59
|
+
const base = new Date(Math.min(...this.rawTasks.map((t) => (t.start as Date).getTime())));
|
|
60
|
+
this.originDate = this.alignOriginByScale(base, this.defaultScale).getTime();
|
|
61
|
+
this.labelConverter = new SlotIndexToDateLabelConverter(
|
|
62
|
+
this.dateMode,
|
|
63
|
+
this.originDate,
|
|
64
|
+
this.scaleSelected.secondes,
|
|
65
|
+
this.hasNegativeDependencySpace
|
|
66
|
+
);
|
|
67
|
+
this.buildTaskSlotsFromDates();
|
|
68
|
+
} else {
|
|
69
|
+
this.labelConverter = new SlotIndexToDateLabelConverter(
|
|
70
|
+
this.dateMode,
|
|
71
|
+
this.originDate,
|
|
72
|
+
this.scaleSelected.secondes,
|
|
73
|
+
this.hasNegativeDependencySpace
|
|
74
|
+
);
|
|
75
|
+
this.buildTaskSlotsFromDuration();
|
|
76
|
+
}
|
|
77
|
+
this.detectNegativeDependencySpace();
|
|
78
|
+
this.calculateTimelineSlots();
|
|
79
|
+
this.getAllDependencyPaths();
|
|
80
|
+
this.ganttHeightCalcul();
|
|
81
|
+
this.ganttWidthCalcul();
|
|
82
|
+
this.recomputeLayout();
|
|
83
|
+
this.ganttScroll.nativeElement.style.height = this.layout.scrollHeight + 'px';
|
|
84
|
+
this.ganttBodyScroll.nativeElement.style.height = this.layout.bodyHeight + 'px';
|
|
85
|
+
this.timelineHeader.nativeElement.style.height = this.headerHeight + 'px';
|
|
86
|
+
this.ganttContent.nativeElement.style.height = this.ganttHeight - this.rowHeight + 'px';
|
|
87
|
+
this.ganttContent.nativeElement.style.width = this.ganttWidth + 'px';
|
|
88
|
+
this.leftPanel.nativeElement.style.width = this.leftPanelWidth + 'px';
|
|
89
|
+
|
|
90
|
+
this.buildViewTasks();
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
this.recenterSelectedTask();
|
|
93
|
+
}, 0);
|
|
94
|
+
this.loading = false;
|
|
95
|
+
this.cdr.markForCheck();
|
|
96
|
+
requestAnimationFrame(() => {
|
|
97
|
+
const end = performance.now();
|
|
98
|
+
console.log('frame time including render:', end - start, 'ms');
|
|
99
|
+
});
|
|
100
|
+
}, 0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
private buildViewTasks(): void {
|
|
105
|
+
const h = this.rowHeight / 1.25;
|
|
106
|
+
|
|
107
|
+
this.viewTasks = this.tasks.map((t): GanttTaskView => {
|
|
108
|
+
return {
|
|
109
|
+
...t,
|
|
110
|
+
x: this.getTaskX(t),
|
|
111
|
+
y: this.getTaskY(t),
|
|
112
|
+
width: this.getTaskWidth(t),
|
|
113
|
+
height: h,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// click on button 10m, hour, months
|
|
119
|
+
public setScale(scale: GanttScale): void {
|
|
120
|
+
this.defaultScale = scale;
|
|
121
|
+
this.recalculteAll();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
// if task was selected scroll back to it
|
|
126
|
+
private recenterSelectedTask(): void {
|
|
127
|
+
if (!this.selectedTaskId) {
|
|
128
|
+
if (!this.ganttScroll) return;
|
|
129
|
+
this.ganttScroll.nativeElement.scrollLeft = 0;
|
|
130
|
+
|
|
131
|
+
// force header sync
|
|
132
|
+
if (this.timelineHeader?.nativeElement) {
|
|
133
|
+
this.timelineHeader.nativeElement.scrollLeft = 0;
|
|
134
|
+
}
|
|
135
|
+
this.onScrolled();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const task = this.tasks.find((t) => t.id === this.selectedTaskId);
|
|
139
|
+
if (!task || !this.ganttScroll) return;
|
|
140
|
+
|
|
141
|
+
// scroll X
|
|
142
|
+
const viewportWidth = this.ganttScroll.nativeElement.clientWidth;
|
|
143
|
+
const targetScrollLeft = this.getTaskX(task) - viewportWidth / 2;
|
|
144
|
+
this.ganttScroll.nativeElement.scrollTo({ left: Math.max(targetScrollLeft, 0), behavior: 'auto' });
|
|
145
|
+
|
|
146
|
+
// scroll Y
|
|
147
|
+
const viewportHeight = this.ganttBodyScroll.nativeElement.clientHeight;
|
|
148
|
+
const targetTop = task.row * this.rowHeight;
|
|
149
|
+
const scrollTop = targetTop - viewportHeight / 2 + this.rowHeight / 2;
|
|
150
|
+
this.ganttBodyScroll.nativeElement.scrollTo({ top: Math.max(scrollTop, 0), behavior: 'auto' });
|
|
151
|
+
|
|
152
|
+
this.onScrolled();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// detect if there is a need to add the N/A slot in order to be able to show dependency arrow before 0.
|
|
156
|
+
private detectNegativeDependencySpace(): void {
|
|
157
|
+
const originTasks = this.tasks.filter((t) => (t.startSlot ?? 0) === 0);
|
|
158
|
+
|
|
159
|
+
this.hasNegativeDependencySpace = originTasks.some((task) => {
|
|
160
|
+
const fromRight = this.getRawTaskX(task) + this.getTaskWidth(task);
|
|
161
|
+
return (task.dependencies ?? []).some((depId: any) => {
|
|
162
|
+
const target = this.tasks.find((t) => t.id === depId);
|
|
163
|
+
if (!target) return false;
|
|
164
|
+
|
|
165
|
+
const toLeft = this.getRawTaskX(target);
|
|
166
|
+
// true if dependency goes "backwards"
|
|
167
|
+
return toLeft < fromRight;
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// scroll buttons left/right => click small go, stay clicked scroll a lot
|
|
173
|
+
public startScroll(direction: 'left' | 'right'): void {
|
|
174
|
+
// instant first move
|
|
175
|
+
this.scrollInterval = setInterval(() => {
|
|
176
|
+
this.ganttScroll.nativeElement.scrollLeft += direction === 'left' ? -100 : 100;
|
|
177
|
+
this.onScrolled();
|
|
178
|
+
}, 50);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// update buttons left/right after a scroll.
|
|
182
|
+
private onScrolled(): void {
|
|
183
|
+
const el = this.ganttScroll.nativeElement;
|
|
184
|
+
const maxScrollLeft = el.scrollWidth - el.clientWidth;
|
|
185
|
+
// left button
|
|
186
|
+
this.showLeftBtn = el.scrollLeft > 0;
|
|
187
|
+
if (this.leftBtn?.nativeElement) {
|
|
188
|
+
if (this.showLeftBtn) {
|
|
189
|
+
this.leftBtn.nativeElement.style.display = 'block';
|
|
190
|
+
this.leftBtn.nativeElement.style.left = this.leftPanel.nativeElement.style.width;
|
|
191
|
+
} else {
|
|
192
|
+
this.leftBtn.nativeElement.style.display = 'none';
|
|
193
|
+
this.leftBtn.nativeElement.style.left = this.leftPanel.nativeElement.style.width;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// right button
|
|
197
|
+
this.showRightBtn = el.scrollLeft < maxScrollLeft - 1 || el.scrollLeft === 0; // small tolerance
|
|
198
|
+
if (this.rightBtn?.nativeElement) {
|
|
199
|
+
if (this.showRightBtn) {
|
|
200
|
+
this.rightBtn.nativeElement.style.display = 'block';
|
|
201
|
+
} else {
|
|
202
|
+
this.rightBtn.nativeElement.style.display = 'none';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!this.showLeftBtn || !this.showRightBtn) {
|
|
206
|
+
this.stopScroll();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public stopScroll(): void{
|
|
211
|
+
clearInterval(this.scrollInterval);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// to sync header and grid
|
|
215
|
+
public onHorizontalScroll(event: Event): void {
|
|
216
|
+
this.timelineHeader.nativeElement.scrollLeft = (event.target as HTMLElement).scrollLeft;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// highlight dependencies (arrows)
|
|
220
|
+
private updateHoveredDependencies(task: GanttTask | null): void {
|
|
221
|
+
this.hoveredDependencyIds.clear();
|
|
222
|
+
if (!task) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// outgoing dependencies
|
|
227
|
+
for (const dep of task.dependencies ?? []) {
|
|
228
|
+
this.hoveredDependencyIds.add(`${dep}->${task.id}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// incoming dependencies
|
|
232
|
+
for (const t of this.tasks) {
|
|
233
|
+
if ((t.dependencies ?? []).includes(task.id)) {
|
|
234
|
+
this.hoveredDependencyIds.add(`${task.id}->${t.id}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// select task left and right panel
|
|
240
|
+
public selectTask(task: GanttTask, scroll: boolean): void {
|
|
241
|
+
if (this.selectedTaskId === task.id) {
|
|
242
|
+
this.selectedTaskId = null;
|
|
243
|
+
this.hoveredDependencyIds.clear();
|
|
244
|
+
this.hideTooltip();
|
|
245
|
+
this.onScrolled();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.selectedTaskId = task.id;
|
|
250
|
+
this.updateHoveredDependencies(task);
|
|
251
|
+
this.onScrolled();
|
|
252
|
+
if (!scroll || !this.ganttScroll) return;
|
|
253
|
+
|
|
254
|
+
const container = this.ganttScroll.nativeElement as HTMLElement;
|
|
255
|
+
const viewportWidth = container.clientWidth;
|
|
256
|
+
const targetScrollLeft = this.getTaskX(task) - viewportWidth / 2;
|
|
257
|
+
|
|
258
|
+
container.scrollTo({
|
|
259
|
+
left: Math.max(targetScrollLeft, 0),
|
|
260
|
+
behavior: 'smooth',
|
|
261
|
+
});
|
|
262
|
+
this.onScrolled();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// resize between panel left and right
|
|
266
|
+
public startResize(event: MouseEvent): void {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
this.resizing = true;
|
|
269
|
+
const containerRect = this.ganttBodyScroll.nativeElement.getBoundingClientRect();
|
|
270
|
+
const left = containerRect.left;
|
|
271
|
+
const min = 50;
|
|
272
|
+
const max = containerRect.width - 75;
|
|
273
|
+
const moveHandler = (e: MouseEvent): void => {
|
|
274
|
+
if (!this.resizing) return;
|
|
275
|
+
const x = e.clientX - left;
|
|
276
|
+
const width = x < min ? min : x > max ? max : x;
|
|
277
|
+
|
|
278
|
+
// direct DOM update (no Angular)
|
|
279
|
+
this.leftPanel.nativeElement.style.width = width + 'px';
|
|
280
|
+
if (this.leftBtn?.nativeElement) {
|
|
281
|
+
this.leftBtn.nativeElement.style.left = `${width}px`;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const upHandler = (): void => {
|
|
286
|
+
this.resizing = false;
|
|
287
|
+
window.removeEventListener('mousemove', moveHandler);
|
|
288
|
+
window.removeEventListener('mouseup', upHandler);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
window.addEventListener('mousemove', moveHandler);
|
|
292
|
+
window.addEventListener('mouseup', upHandler);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public showTooltip(event: MouseEvent, task: GanttTask): void {
|
|
296
|
+
this.tooltipTask = task;
|
|
297
|
+
const tooltip = document.querySelector('.global-tooltip') as HTMLElement;
|
|
298
|
+
if (!tooltip) return;
|
|
299
|
+
tooltip.style.left = `${event.clientX + 10}px`;
|
|
300
|
+
tooltip.style.top = `${event.clientY - 50}px`;
|
|
301
|
+
tooltip.style.display = 'block';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public hideTooltip(): void {
|
|
305
|
+
this.tooltipTask = null;
|
|
306
|
+
const tooltip = document.querySelector('.global-tooltip') as HTMLElement;
|
|
307
|
+
if (!tooltip) return;
|
|
308
|
+
tooltip.style.display = 'none';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private buildTaskSlotsFromDates(): void {
|
|
312
|
+
const slotMs = this.scaleSelected.secondes * 1000;
|
|
313
|
+
this.tasks = this.rawTasks.map((t) => {
|
|
314
|
+
if (!(t.start instanceof Date) || !(t.end instanceof Date)) {
|
|
315
|
+
throw new Error(`Task "${t.id}" must have Date start/end values.`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const startSlot = ((t.start as Date).getTime() - this.originDate) / slotMs;
|
|
319
|
+
const endSlot = ((t.end as Date).getTime() - this.originDate) / slotMs;
|
|
320
|
+
return { ...t, startSlot, endSlot };
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private buildTaskSlotsFromDuration(): void {
|
|
325
|
+
this.tasks = this.rawTasks.map((t) => {
|
|
326
|
+
if (typeof t.start !== 'number' || typeof t.end !== 'number') {
|
|
327
|
+
throw new Error(`Task "${t.id}" must have numeric start/end values expressed in minutes.`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const startSlot = t.start / this.scaleSelected.secondes;
|
|
331
|
+
const endSlot = t.end / this.scaleSelected.secondes;
|
|
332
|
+
return { ...t, startSlot, endSlot };
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// get all slots (timeline)
|
|
337
|
+
private calculateTimelineSlots(): void {
|
|
338
|
+
const maxSlot = Math.max(...this.tasks.map(t => t.endSlot ?? 0));
|
|
339
|
+
if (maxSlot > 53000) {
|
|
340
|
+
this.maxSlotsAlert = true;
|
|
341
|
+
this.slots = [];
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.maxSlotsAlert = false;
|
|
345
|
+
const buffer = [];
|
|
346
|
+
let offsetX = 0;
|
|
347
|
+
|
|
348
|
+
if (this.hasNegativeDependencySpace) {
|
|
349
|
+
buffer.push({
|
|
350
|
+
index: -1,
|
|
351
|
+
x: 0,
|
|
352
|
+
width: this.negativeSlotWidth,
|
|
353
|
+
label: 'N/A'
|
|
354
|
+
});
|
|
355
|
+
offsetX = this.negativeSlotWidth;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (let i = 0; i <= maxSlot + 2; i++) {
|
|
359
|
+
buffer.push({
|
|
360
|
+
index: i,
|
|
361
|
+
x: offsetX + i * this.scaleSelected.px,
|
|
362
|
+
width: this.scaleSelected.px,
|
|
363
|
+
label: this.getSlotLabel(i) ?? 'Error',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.slots = buffer;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// get all paths of arrows
|
|
371
|
+
protected getAllDependencyPaths(): void {
|
|
372
|
+
this.dependencyPaths = this.tasks.flatMap((task): DependencyPath[] =>
|
|
373
|
+
(task.dependencies ?? []).map(
|
|
374
|
+
(dep: any): DependencyPath => ({
|
|
375
|
+
from: dep,
|
|
376
|
+
to: task.id,
|
|
377
|
+
id: `${dep}->${task.id}`,
|
|
378
|
+
path: this.getDependencyPath(dep, task.id),
|
|
379
|
+
})
|
|
380
|
+
)
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// get label for date header
|
|
385
|
+
private getSlotLabel(index: number): string {
|
|
386
|
+
if (this.dataType === 'calendar') {
|
|
387
|
+
return this.labelConverter?.getLabelCalendar(index, this.defaultScale) ?? 'Error';
|
|
388
|
+
}
|
|
389
|
+
return this.labelConverter?.getLabelDuration(index, this.defaultScale) ?? 'Error';
|
|
390
|
+
}
|
|
391
|
+
}
|