@canvas-components/gantt-table 0.1.1 → 0.1.2
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/README.md +184 -1
- package/dist/index.cjs +2321 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -21
- package/dist/index.d.ts +218 -21
- package/dist/index.js +2314 -162
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4,10 +4,199 @@ var __defProp = Object.defineProperty;
|
|
|
4
4
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
5
5
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
6
6
|
|
|
7
|
+
// src/domain/scroll.ts
|
|
8
|
+
var GANTT_SCROLLBAR_SIZE = 10;
|
|
9
|
+
function resolveGanttViewportLayout(layout, width, height) {
|
|
10
|
+
const timelineContentWidth = Math.max(
|
|
11
|
+
layout.totalWidth - layout.taskColumnWidth,
|
|
12
|
+
0
|
|
13
|
+
);
|
|
14
|
+
const bodyContentHeight = Math.max(
|
|
15
|
+
layout.totalHeight - layout.headerHeight,
|
|
16
|
+
0
|
|
17
|
+
);
|
|
18
|
+
let hasHorizontalScrollbar = timelineContentWidth > Math.max(width - layout.taskColumnWidth, 0);
|
|
19
|
+
let hasVerticalScrollbar = bodyContentHeight > Math.max(height - layout.headerHeight, 0);
|
|
20
|
+
for (let index = 0; index < 2; index += 1) {
|
|
21
|
+
const timelineViewportWidth2 = Math.max(
|
|
22
|
+
width - layout.taskColumnWidth - (hasVerticalScrollbar ? GANTT_SCROLLBAR_SIZE : 0),
|
|
23
|
+
0
|
|
24
|
+
);
|
|
25
|
+
const bodyViewportHeight2 = Math.max(
|
|
26
|
+
height - layout.headerHeight - (hasHorizontalScrollbar ? GANTT_SCROLLBAR_SIZE : 0),
|
|
27
|
+
0
|
|
28
|
+
);
|
|
29
|
+
hasHorizontalScrollbar = timelineContentWidth > timelineViewportWidth2 + 1;
|
|
30
|
+
hasVerticalScrollbar = bodyContentHeight > bodyViewportHeight2 + 1;
|
|
31
|
+
}
|
|
32
|
+
const contentRight = Math.max(
|
|
33
|
+
width - (hasVerticalScrollbar ? GANTT_SCROLLBAR_SIZE : 0),
|
|
34
|
+
layout.taskColumnWidth
|
|
35
|
+
);
|
|
36
|
+
const contentBottom = Math.max(
|
|
37
|
+
height - (hasHorizontalScrollbar ? GANTT_SCROLLBAR_SIZE : 0),
|
|
38
|
+
layout.headerHeight
|
|
39
|
+
);
|
|
40
|
+
const timelineViewportWidth = Math.max(
|
|
41
|
+
contentRight - layout.taskColumnWidth,
|
|
42
|
+
0
|
|
43
|
+
);
|
|
44
|
+
const bodyViewportHeight = Math.max(contentBottom - layout.headerHeight, 0);
|
|
45
|
+
return {
|
|
46
|
+
width,
|
|
47
|
+
height,
|
|
48
|
+
contentRight,
|
|
49
|
+
contentBottom,
|
|
50
|
+
timelineViewportWidth,
|
|
51
|
+
bodyViewportHeight,
|
|
52
|
+
hasHorizontalScrollbar,
|
|
53
|
+
hasVerticalScrollbar,
|
|
54
|
+
bounds: {
|
|
55
|
+
maxLeft: Math.max(timelineContentWidth - timelineViewportWidth, 0),
|
|
56
|
+
maxTop: Math.max(bodyContentHeight - bodyViewportHeight, 0)
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function resolveGanttScrollBounds(layout, width, height) {
|
|
61
|
+
return resolveGanttViewportLayout(layout, width, height).bounds;
|
|
62
|
+
}
|
|
63
|
+
function clampGanttScroll(scroll, bounds) {
|
|
64
|
+
return {
|
|
65
|
+
left: Math.min(Math.max(scroll.left, 0), bounds.maxLeft),
|
|
66
|
+
top: Math.min(Math.max(scroll.top, 0), bounds.maxTop)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/domain/scrollbar.ts
|
|
71
|
+
var MIN_THUMB_SIZE = 36;
|
|
72
|
+
function computeGanttScrollbarLayout(params) {
|
|
73
|
+
const { layout, viewport, scroll } = params;
|
|
74
|
+
const result = {};
|
|
75
|
+
if (viewport.hasHorizontalScrollbar) {
|
|
76
|
+
const barX = layout.taskColumnWidth;
|
|
77
|
+
const barWidth = Math.max(viewport.contentRight - barX, 0);
|
|
78
|
+
const decreaseButton = {
|
|
79
|
+
x: barX,
|
|
80
|
+
y: viewport.contentBottom,
|
|
81
|
+
width: Math.min(GANTT_SCROLLBAR_SIZE, barWidth / 2),
|
|
82
|
+
height: GANTT_SCROLLBAR_SIZE
|
|
83
|
+
};
|
|
84
|
+
const increaseButton = {
|
|
85
|
+
x: Math.max(viewport.contentRight - GANTT_SCROLLBAR_SIZE, barX),
|
|
86
|
+
y: viewport.contentBottom,
|
|
87
|
+
width: Math.min(GANTT_SCROLLBAR_SIZE, barWidth / 2),
|
|
88
|
+
height: GANTT_SCROLLBAR_SIZE
|
|
89
|
+
};
|
|
90
|
+
const track = {
|
|
91
|
+
x: decreaseButton.x + decreaseButton.width,
|
|
92
|
+
y: viewport.contentBottom,
|
|
93
|
+
width: Math.max(
|
|
94
|
+
barWidth - decreaseButton.width - increaseButton.width,
|
|
95
|
+
0
|
|
96
|
+
),
|
|
97
|
+
height: GANTT_SCROLLBAR_SIZE
|
|
98
|
+
};
|
|
99
|
+
const contentWidth = Math.max(
|
|
100
|
+
layout.totalWidth - layout.taskColumnWidth,
|
|
101
|
+
1
|
|
102
|
+
);
|
|
103
|
+
const thumbWidth = Math.min(
|
|
104
|
+
Math.max(
|
|
105
|
+
viewport.timelineViewportWidth / contentWidth * track.width,
|
|
106
|
+
MIN_THUMB_SIZE
|
|
107
|
+
),
|
|
108
|
+
track.width
|
|
109
|
+
);
|
|
110
|
+
const available = Math.max(track.width - thumbWidth, 0);
|
|
111
|
+
result.horizontal = {
|
|
112
|
+
decreaseButton,
|
|
113
|
+
increaseButton,
|
|
114
|
+
track,
|
|
115
|
+
thumb: {
|
|
116
|
+
x: track.x + (viewport.bounds.maxLeft === 0 ? 0 : scroll.left / viewport.bounds.maxLeft * available),
|
|
117
|
+
y: track.y,
|
|
118
|
+
width: thumbWidth,
|
|
119
|
+
height: track.height
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (viewport.hasVerticalScrollbar) {
|
|
124
|
+
const barY = layout.headerHeight;
|
|
125
|
+
const barHeight = Math.max(viewport.contentBottom - barY, 0);
|
|
126
|
+
const decreaseButton = {
|
|
127
|
+
x: viewport.contentRight,
|
|
128
|
+
y: barY,
|
|
129
|
+
width: GANTT_SCROLLBAR_SIZE,
|
|
130
|
+
height: Math.min(GANTT_SCROLLBAR_SIZE, barHeight / 2)
|
|
131
|
+
};
|
|
132
|
+
const increaseButton = {
|
|
133
|
+
x: viewport.contentRight,
|
|
134
|
+
y: Math.max(viewport.contentBottom - GANTT_SCROLLBAR_SIZE, barY),
|
|
135
|
+
width: GANTT_SCROLLBAR_SIZE,
|
|
136
|
+
height: Math.min(GANTT_SCROLLBAR_SIZE, barHeight / 2)
|
|
137
|
+
};
|
|
138
|
+
const track = {
|
|
139
|
+
x: viewport.contentRight,
|
|
140
|
+
y: decreaseButton.y + decreaseButton.height,
|
|
141
|
+
width: GANTT_SCROLLBAR_SIZE,
|
|
142
|
+
height: Math.max(
|
|
143
|
+
barHeight - decreaseButton.height - increaseButton.height,
|
|
144
|
+
0
|
|
145
|
+
)
|
|
146
|
+
};
|
|
147
|
+
const contentHeight = Math.max(layout.totalHeight - layout.headerHeight, 1);
|
|
148
|
+
const thumbHeight = Math.min(
|
|
149
|
+
Math.max(
|
|
150
|
+
viewport.bodyViewportHeight / contentHeight * track.height,
|
|
151
|
+
MIN_THUMB_SIZE
|
|
152
|
+
),
|
|
153
|
+
track.height
|
|
154
|
+
);
|
|
155
|
+
const available = Math.max(track.height - thumbHeight, 0);
|
|
156
|
+
result.vertical = {
|
|
157
|
+
decreaseButton,
|
|
158
|
+
increaseButton,
|
|
159
|
+
track,
|
|
160
|
+
thumb: {
|
|
161
|
+
x: track.x,
|
|
162
|
+
y: track.y + (viewport.bounds.maxTop === 0 ? 0 : scroll.top / viewport.bounds.maxTop * available),
|
|
163
|
+
width: track.width,
|
|
164
|
+
height: thumbHeight
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
function hitTestGanttScrollbar(x, y, layout) {
|
|
171
|
+
for (const axis of ["horizontal", "vertical"]) {
|
|
172
|
+
const scrollbar = layout[axis];
|
|
173
|
+
if (!scrollbar) continue;
|
|
174
|
+
for (const role of [
|
|
175
|
+
"thumb",
|
|
176
|
+
"decrease-button",
|
|
177
|
+
"increase-button",
|
|
178
|
+
"track"
|
|
179
|
+
]) {
|
|
180
|
+
const rect = role === "decrease-button" ? scrollbar.decreaseButton : role === "increase-button" ? scrollbar.increaseButton : scrollbar[role];
|
|
181
|
+
if (isPointInRect(x, y, rect)) return { axis, role };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function isPointInRect(x, y, rect) {
|
|
187
|
+
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
188
|
+
}
|
|
189
|
+
|
|
7
190
|
// src/domain/date.ts
|
|
8
191
|
var toTimestamp = (value) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
192
|
+
const dateOnlyMatch = typeof value === "string" ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) : null;
|
|
193
|
+
const timestamp = dateOnlyMatch ? new Date(
|
|
194
|
+
Number(dateOnlyMatch[1]),
|
|
195
|
+
Number(dateOnlyMatch[2]) - 1,
|
|
196
|
+
Number(dateOnlyMatch[3])
|
|
197
|
+
).getTime() : value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
198
|
+
if (!Number.isFinite(timestamp))
|
|
199
|
+
throw new Error(`Invalid date value: ${String(value)}`);
|
|
11
200
|
return timestamp;
|
|
12
201
|
};
|
|
13
202
|
var dayMs = 24 * 60 * 60 * 1e3;
|
|
@@ -16,35 +205,1944 @@ function startOfDay(timestamp) {
|
|
|
16
205
|
date.setHours(0, 0, 0, 0);
|
|
17
206
|
return date.getTime();
|
|
18
207
|
}
|
|
19
|
-
|
|
20
|
-
|
|
208
|
+
|
|
209
|
+
// src/domain/task-rows.ts
|
|
210
|
+
function resolveGanttRowTasks(row, multiTask) {
|
|
211
|
+
return multiTask ? row.tasks ?? [] : [row];
|
|
212
|
+
}
|
|
213
|
+
function findGanttTaskById(rows, taskId) {
|
|
214
|
+
if (!taskId) return null;
|
|
215
|
+
for (const row of rows) {
|
|
216
|
+
if (row.id === taskId) return row;
|
|
217
|
+
const task = row.tasks?.find((item) => item.id === taskId);
|
|
218
|
+
if (task) return task;
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
function findGanttLayoutTask(layout, taskId) {
|
|
223
|
+
if (!taskId) return null;
|
|
224
|
+
for (const row of layout.rows) {
|
|
225
|
+
const task = row.tasks.find((item) => item.id === taskId);
|
|
226
|
+
if (task) return { row, task };
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
21
229
|
}
|
|
22
230
|
|
|
23
231
|
// src/domain/layout.ts
|
|
24
232
|
function resolveDayWidth(scale) {
|
|
25
|
-
return scale === "month" ?
|
|
233
|
+
return scale === "month" ? 12 : scale === "week" ? 28 : 80;
|
|
26
234
|
}
|
|
27
|
-
function buildGanttLayout(options,
|
|
28
|
-
const timestamps = options.tasks.flatMap(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
235
|
+
function buildGanttLayout(options, _width) {
|
|
236
|
+
const timestamps = options.tasks.flatMap(
|
|
237
|
+
(row) => resolveGanttRowTasks(row, options.multiTask === true).flatMap(
|
|
238
|
+
(task) => task.start != null && task.end != null ? [toTimestamp(task.start), toTimestamp(task.end)] : []
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
const fallbackStart = timestamps.length > 0 ? Math.min(...timestamps) : Date.now();
|
|
242
|
+
const fallbackEnd = timestamps.length > 0 ? Math.max(...timestamps) : fallbackStart + dayMs;
|
|
243
|
+
const rawStart = startOfDay(toTimestamp(options.startDate ?? fallbackStart));
|
|
244
|
+
const rawEnd = startOfDay(
|
|
245
|
+
toTimestamp(options.endDate ?? fallbackEnd + dayMs)
|
|
246
|
+
);
|
|
247
|
+
const scale = options.scale ?? "day";
|
|
248
|
+
const { start, end } = resolveScaleRange(rawStart, rawEnd, scale);
|
|
249
|
+
const dayWidth = resolveDayWidth(scale);
|
|
32
250
|
const days = Math.max(1, Math.ceil((end - start) / dayMs));
|
|
33
251
|
const taskColumnWidth = options.taskColumnWidth ?? 220;
|
|
34
|
-
const
|
|
35
|
-
|
|
252
|
+
const headerLevels = resolveHeaderLevelLayouts(options, scale);
|
|
253
|
+
const headerHeight = headerLevels.reduce(
|
|
254
|
+
(total, level) => total + level.height,
|
|
255
|
+
0
|
|
256
|
+
);
|
|
257
|
+
const rows = options.tasks.map((task, index) => ({
|
|
258
|
+
task,
|
|
259
|
+
tasks: resolveGanttRowTasks(task, options.multiTask === true),
|
|
260
|
+
index,
|
|
261
|
+
y: headerHeight + index * (options.rowHeight ?? 36)
|
|
262
|
+
}));
|
|
263
|
+
return {
|
|
264
|
+
scale,
|
|
265
|
+
start,
|
|
266
|
+
end,
|
|
267
|
+
dayWidth,
|
|
268
|
+
rowHeight: options.rowHeight ?? 36,
|
|
269
|
+
headerHeight,
|
|
270
|
+
headerLevels,
|
|
271
|
+
taskColumnWidth,
|
|
272
|
+
rows,
|
|
273
|
+
totalWidth: taskColumnWidth + days * dayWidth,
|
|
274
|
+
totalHeight: headerHeight + options.tasks.length * (options.rowHeight ?? 36)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function fitGanttLayoutToTimelineWidth(layout, timelineViewportWidth) {
|
|
278
|
+
const timelineWidth = Math.max(layout.totalWidth - layout.taskColumnWidth, 0);
|
|
279
|
+
if (timelineViewportWidth <= timelineWidth || timelineViewportWidth <= 0) {
|
|
280
|
+
return layout;
|
|
281
|
+
}
|
|
282
|
+
const days = Math.max(1, Math.ceil((layout.end - layout.start) / dayMs));
|
|
283
|
+
return {
|
|
284
|
+
...layout,
|
|
285
|
+
dayWidth: timelineViewportWidth / days,
|
|
286
|
+
totalWidth: layout.taskColumnWidth + timelineViewportWidth
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function resolveHeaderLevelLayouts(options, scale) {
|
|
290
|
+
const defaultHeight = Math.max(options.headerHeight ?? 56, 1);
|
|
291
|
+
const levels = options.headerLevels?.length ? options.headerLevels : [{ unit: scale }];
|
|
292
|
+
let y = 0;
|
|
293
|
+
return levels.map((level) => {
|
|
294
|
+
const height = Math.max(level.height ?? defaultHeight, 1);
|
|
295
|
+
const result = { ...level, y, height };
|
|
296
|
+
y += height;
|
|
297
|
+
return result;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function resolveScaleRange(start, end, scale) {
|
|
301
|
+
if (scale === "day") return { start, end };
|
|
302
|
+
const startDate = new Date(start);
|
|
303
|
+
const endDate = new Date(end);
|
|
304
|
+
if (scale === "week") {
|
|
305
|
+
const startDay = (startDate.getDay() + 6) % 7;
|
|
306
|
+
startDate.setDate(startDate.getDate() - startDay);
|
|
307
|
+
const endDay = (endDate.getDay() + 6) % 7;
|
|
308
|
+
if (endDay !== 0) endDate.setDate(endDate.getDate() + 7 - endDay);
|
|
309
|
+
return { start: startDate.getTime(), end: endDate.getTime() };
|
|
310
|
+
}
|
|
311
|
+
startDate.setDate(1);
|
|
312
|
+
if (endDate.getDate() !== 1) {
|
|
313
|
+
endDate.setMonth(endDate.getMonth() + 1, 1);
|
|
314
|
+
}
|
|
315
|
+
return { start: startDate.getTime(), end: endDate.getTime() };
|
|
36
316
|
}
|
|
37
317
|
function xForDate(timestamp, layout) {
|
|
38
318
|
return layout.taskColumnWidth + (timestamp - layout.start) / dayMs * layout.dayWidth;
|
|
39
319
|
}
|
|
40
320
|
|
|
41
|
-
// src/
|
|
42
|
-
|
|
43
|
-
|
|
321
|
+
// src/domain/task-geometry.ts
|
|
322
|
+
var RESIZE_HANDLE_HIT_WIDTH = 8;
|
|
323
|
+
function hasTaskSchedule(task) {
|
|
324
|
+
return task.start != null && task.end != null;
|
|
325
|
+
}
|
|
326
|
+
function clearTaskSchedule(task) {
|
|
327
|
+
delete task.start;
|
|
328
|
+
delete task.end;
|
|
329
|
+
}
|
|
330
|
+
function resolveTaskBarRect(task, rowY, layout) {
|
|
331
|
+
if (!hasTaskSchedule(task)) {
|
|
332
|
+
throw new Error(`Task ${task.id} does not have a schedule.`);
|
|
333
|
+
}
|
|
334
|
+
const start = toTimestamp(task.start);
|
|
335
|
+
const end = Math.max(start + dayMs, toTimestamp(task.end));
|
|
336
|
+
return {
|
|
337
|
+
x: xForDate(start, layout),
|
|
338
|
+
y: rowY + 8,
|
|
339
|
+
width: Math.max(8, (end - start) / dayMs * layout.dayWidth),
|
|
340
|
+
height: Math.max(layout.rowHeight - 16, 4)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function hitTestTaskBar(params) {
|
|
344
|
+
const row = params.layout.rows.find(
|
|
345
|
+
(item) => params.y >= item.y && params.y < item.y + params.layout.rowHeight
|
|
346
|
+
);
|
|
44
347
|
if (!row) return null;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
348
|
+
for (let index = row.tasks.length - 1; index >= 0; index -= 1) {
|
|
349
|
+
const task = row.tasks[index];
|
|
350
|
+
if (!task || !hasTaskSchedule(task)) continue;
|
|
351
|
+
const rect = resolveTaskBarRect(task, row.y, params.layout);
|
|
352
|
+
if (params.y < rect.y || params.y > rect.y + rect.height || params.x < rect.x - RESIZE_HANDLE_HIT_WIDTH / 2 || params.x > rect.x + rect.width + RESIZE_HANDLE_HIT_WIDTH / 2) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (params.resizable && task.id === params.selectedId) {
|
|
356
|
+
if (Math.abs(params.x - rect.x) <= RESIZE_HANDLE_HIT_WIDTH) {
|
|
357
|
+
return { task, role: "resize-start" };
|
|
358
|
+
}
|
|
359
|
+
if (Math.abs(params.x - (rect.x + rect.width)) <= RESIZE_HANDLE_HIT_WIDTH) {
|
|
360
|
+
return { task, role: "resize-end" };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { task, role: "body" };
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
function resolveTaskCreateRange(params) {
|
|
368
|
+
const lastDayStart = Math.max(params.layout.end - dayMs, params.layout.start);
|
|
369
|
+
const timestampForX = (x) => {
|
|
370
|
+
const dayIndex = Math.floor(
|
|
371
|
+
(x - params.layout.taskColumnWidth) / params.layout.dayWidth
|
|
372
|
+
);
|
|
373
|
+
return Math.min(
|
|
374
|
+
Math.max(params.layout.start + dayIndex * dayMs, params.layout.start),
|
|
375
|
+
lastDayStart
|
|
376
|
+
);
|
|
377
|
+
};
|
|
378
|
+
const anchor = timestampForX(params.anchorX);
|
|
379
|
+
const current = timestampForX(params.currentX);
|
|
380
|
+
return {
|
|
381
|
+
start: Math.min(anchor, current),
|
|
382
|
+
end: Math.min(Math.max(anchor, current) + dayMs, params.layout.end)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function resolveTaskTimeDelta(deltaX, layout, snapToColumn, anchorTimestamp) {
|
|
386
|
+
const rawDays = deltaX / layout.dayWidth;
|
|
387
|
+
const rawDelta = rawDays * dayMs;
|
|
388
|
+
if (!snapToColumn) return rawDelta;
|
|
389
|
+
const anchor = anchorTimestamp ?? layout.start;
|
|
390
|
+
const target = anchor + rawDelta;
|
|
391
|
+
const snappedTarget = layout.start + Math.round((target - layout.start) / dayMs) * dayMs;
|
|
392
|
+
return snappedTarget - anchor;
|
|
393
|
+
}
|
|
394
|
+
function resolveTaskDragRange(params) {
|
|
395
|
+
const { mode, originalStart, originalEnd } = params;
|
|
396
|
+
const minStart = params.minStart ?? Number.NEGATIVE_INFINITY;
|
|
397
|
+
const maxEnd = params.maxEnd ?? Number.POSITIVE_INFINITY;
|
|
398
|
+
if (mode === "resize-start") {
|
|
399
|
+
return {
|
|
400
|
+
start: Math.max(
|
|
401
|
+
minStart,
|
|
402
|
+
Math.min(originalStart + params.delta, originalEnd - dayMs)
|
|
403
|
+
),
|
|
404
|
+
end: originalEnd
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (mode === "resize-end") {
|
|
408
|
+
return {
|
|
409
|
+
start: originalStart,
|
|
410
|
+
end: Math.min(
|
|
411
|
+
maxEnd,
|
|
412
|
+
Math.max(originalEnd + params.delta, originalStart + dayMs)
|
|
413
|
+
)
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const delta = Math.min(
|
|
417
|
+
Math.max(params.delta, minStart - originalStart),
|
|
418
|
+
maxEnd - originalEnd
|
|
419
|
+
);
|
|
420
|
+
return {
|
|
421
|
+
start: originalStart + delta,
|
|
422
|
+
end: originalEnd + delta
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/domain/dependency-geometry.ts
|
|
427
|
+
var CONNECTOR_GAP = 12;
|
|
428
|
+
function resolveDependencyRoute(params) {
|
|
429
|
+
const type = params.type ?? "finish-to-start";
|
|
430
|
+
const fromY = params.from.y + params.from.height / 2;
|
|
431
|
+
const toY = params.to.y + params.to.height / 2;
|
|
432
|
+
const fromLeft = params.from.x;
|
|
433
|
+
const fromRight = params.from.x + params.from.width;
|
|
434
|
+
const toLeft = params.to.x;
|
|
435
|
+
const toRight = params.to.x + params.to.width;
|
|
436
|
+
if (type === "start-to-start") {
|
|
437
|
+
const outerX = Math.min(fromLeft, toLeft) - CONNECTOR_GAP;
|
|
438
|
+
return {
|
|
439
|
+
points: [
|
|
440
|
+
{ x: fromLeft, y: fromY },
|
|
441
|
+
{ x: outerX, y: fromY },
|
|
442
|
+
{ x: outerX, y: toY },
|
|
443
|
+
{ x: toLeft, y: toY }
|
|
444
|
+
],
|
|
445
|
+
arrowDirection: "right"
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (type === "finish-to-finish") {
|
|
449
|
+
const outerX = Math.max(fromRight, toRight) + CONNECTOR_GAP;
|
|
450
|
+
return {
|
|
451
|
+
points: [
|
|
452
|
+
{ x: fromRight, y: fromY },
|
|
453
|
+
{ x: outerX, y: fromY },
|
|
454
|
+
{ x: outerX, y: toY },
|
|
455
|
+
{ x: toRight, y: toY }
|
|
456
|
+
],
|
|
457
|
+
arrowDirection: "left"
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const sourceExitX = fromRight + CONNECTOR_GAP;
|
|
461
|
+
const targetEntryX = toLeft - CONNECTOR_GAP;
|
|
462
|
+
if (sourceExitX <= targetEntryX) {
|
|
463
|
+
const middleX = (sourceExitX + targetEntryX) / 2;
|
|
464
|
+
return {
|
|
465
|
+
points: [
|
|
466
|
+
{ x: fromRight, y: fromY },
|
|
467
|
+
{ x: middleX, y: fromY },
|
|
468
|
+
{ x: middleX, y: toY },
|
|
469
|
+
{ x: toLeft, y: toY }
|
|
470
|
+
],
|
|
471
|
+
arrowDirection: "right"
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const rowDirection = toY >= fromY ? 1 : -1;
|
|
475
|
+
const channelY = fromY === toY ? fromY - Math.max(params.rowHeight / 2, CONNECTOR_GAP) : fromY + params.rowHeight / 2 * rowDirection;
|
|
476
|
+
return {
|
|
477
|
+
points: [
|
|
478
|
+
{ x: fromRight, y: fromY },
|
|
479
|
+
{ x: sourceExitX, y: fromY },
|
|
480
|
+
{ x: sourceExitX, y: channelY },
|
|
481
|
+
{ x: targetEntryX, y: channelY },
|
|
482
|
+
{ x: targetEntryX, y: toY },
|
|
483
|
+
{ x: toLeft, y: toY }
|
|
484
|
+
],
|
|
485
|
+
arrowDirection: "right"
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function resolveDependencyLayoutRoute(dependency, layout, rowOffsets) {
|
|
489
|
+
const from = findGanttLayoutTask(layout, dependency.from);
|
|
490
|
+
const to = findGanttLayoutTask(layout, dependency.to);
|
|
491
|
+
if (!from || !to || !hasTaskSchedule(from.task) || !hasTaskSchedule(to.task)) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return resolveDependencyRoute({
|
|
495
|
+
from: resolveTaskBarRect(
|
|
496
|
+
from.task,
|
|
497
|
+
from.row.y + (rowOffsets?.get(from.row.task.id) ?? 0),
|
|
498
|
+
layout
|
|
499
|
+
),
|
|
500
|
+
to: resolveTaskBarRect(
|
|
501
|
+
to.task,
|
|
502
|
+
to.row.y + (rowOffsets?.get(to.row.task.id) ?? 0),
|
|
503
|
+
layout
|
|
504
|
+
),
|
|
505
|
+
rowHeight: layout.rowHeight,
|
|
506
|
+
type: dependency.type
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function hitTestGanttDependency(params) {
|
|
510
|
+
const tolerance = params.tolerance ?? 6;
|
|
511
|
+
for (let index = params.dependencies.length - 1; index >= 0; index -= 1) {
|
|
512
|
+
const dependency = params.dependencies[index];
|
|
513
|
+
if (!dependency) continue;
|
|
514
|
+
const route = resolveDependencyLayoutRoute(dependency, params.layout);
|
|
515
|
+
if (!route) continue;
|
|
516
|
+
for (let pointIndex = 1; pointIndex < route.points.length; pointIndex += 1) {
|
|
517
|
+
const start = route.points[pointIndex - 1];
|
|
518
|
+
const end = route.points[pointIndex];
|
|
519
|
+
if (start && end && distanceToSegment(params.x, params.y, start.x, start.y, end.x, end.y) <= tolerance) {
|
|
520
|
+
return { dependency, index, route };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
function distanceToSegment(x, y, startX, startY, endX, endY) {
|
|
527
|
+
const deltaX = endX - startX;
|
|
528
|
+
const deltaY = endY - startY;
|
|
529
|
+
const lengthSquared = deltaX * deltaX + deltaY * deltaY;
|
|
530
|
+
if (lengthSquared === 0) return Math.hypot(x - startX, y - startY);
|
|
531
|
+
const ratio = Math.min(
|
|
532
|
+
Math.max(
|
|
533
|
+
((x - startX) * deltaX + (y - startY) * deltaY) / lengthSquared,
|
|
534
|
+
0
|
|
535
|
+
),
|
|
536
|
+
1
|
|
537
|
+
);
|
|
538
|
+
return Math.hypot(
|
|
539
|
+
x - (startX + ratio * deltaX),
|
|
540
|
+
y - (startY + ratio * deltaY)
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/domain/dependency-edit.ts
|
|
545
|
+
var CONNECTOR_OFFSET = 10;
|
|
546
|
+
var CONNECTOR_HIT_RADIUS = 8;
|
|
547
|
+
function resolveTaskDependencyAnchors(task, rowY, layout) {
|
|
548
|
+
if (!hasTaskSchedule(task)) return null;
|
|
549
|
+
const rect = resolveTaskBarRect(task, rowY, layout);
|
|
550
|
+
const y = rect.y + rect.height / 2;
|
|
551
|
+
return [
|
|
552
|
+
{ task, side: "start", x: rect.x - CONNECTOR_OFFSET, y },
|
|
553
|
+
{
|
|
554
|
+
task,
|
|
555
|
+
side: "finish",
|
|
556
|
+
x: rect.x + rect.width + CONNECTOR_OFFSET,
|
|
557
|
+
y
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
function hitTestTaskDependencyAnchor(params) {
|
|
562
|
+
const result = findGanttLayoutTask(params.layout, params.selectedTaskId);
|
|
563
|
+
if (!result) return null;
|
|
564
|
+
const anchors = resolveTaskDependencyAnchors(
|
|
565
|
+
result.task,
|
|
566
|
+
result.row.y,
|
|
567
|
+
params.layout
|
|
568
|
+
);
|
|
569
|
+
return anchors?.find(
|
|
570
|
+
(anchor) => Math.hypot(params.x - anchor.x, params.y - anchor.y) <= CONNECTOR_HIT_RADIUS
|
|
571
|
+
) ?? null;
|
|
572
|
+
}
|
|
573
|
+
function resolveDependencyType(sourceSide, targetSide) {
|
|
574
|
+
if (sourceSide === "start") {
|
|
575
|
+
return targetSide === "start" ? "start-to-start" : null;
|
|
576
|
+
}
|
|
577
|
+
return targetSide === "finish" ? "finish-to-finish" : "finish-to-start";
|
|
578
|
+
}
|
|
579
|
+
function resolveDependencyDropTarget(params) {
|
|
580
|
+
const row = params.layout.rows.find(
|
|
581
|
+
(item) => params.y >= item.y && params.y < item.y + params.layout.rowHeight
|
|
582
|
+
);
|
|
583
|
+
if (!row) return null;
|
|
584
|
+
for (let index = row.tasks.length - 1; index >= 0; index -= 1) {
|
|
585
|
+
const task = row.tasks[index];
|
|
586
|
+
if (!task || task.id === params.sourceTaskId || !hasTaskSchedule(task)) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const rect = resolveTaskBarRect(task, row.y, params.layout);
|
|
590
|
+
if (params.x < rect.x - CONNECTOR_HIT_RADIUS || params.x > rect.x + rect.width + CONNECTOR_HIT_RADIUS) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const targetSide = params.x < rect.x + rect.width / 2 ? "start" : "finish";
|
|
594
|
+
const type = resolveDependencyType(params.sourceSide, targetSide);
|
|
595
|
+
if (!type) continue;
|
|
596
|
+
return {
|
|
597
|
+
task,
|
|
598
|
+
side: targetSide,
|
|
599
|
+
type,
|
|
600
|
+
x: targetSide === "start" ? rect.x : rect.x + rect.width,
|
|
601
|
+
y: rect.y + rect.height / 2
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
function upsertGanttDependency(dependencies, dependency) {
|
|
607
|
+
const index = dependencies.findIndex(
|
|
608
|
+
(item) => item.from === dependency.from && item.to === dependency.to
|
|
609
|
+
);
|
|
610
|
+
if (index < 0) {
|
|
611
|
+
return {
|
|
612
|
+
dependencies: [...dependencies, dependency],
|
|
613
|
+
index: dependencies.length
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
const next = dependencies.slice();
|
|
617
|
+
next[index] = { ...dependencies[index], ...dependency };
|
|
618
|
+
return { dependencies: next, index };
|
|
619
|
+
}
|
|
620
|
+
function removeGanttDependency(dependencies, index) {
|
|
621
|
+
return dependencies.filter((_, itemIndex) => itemIndex !== index);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/domain/row-drag.ts
|
|
625
|
+
function resolveRowDragTargetIndex(params) {
|
|
626
|
+
const rowHeight = Math.max(params.rowHeight, 1);
|
|
627
|
+
return Math.min(
|
|
628
|
+
Math.max(
|
|
629
|
+
Math.round(params.fromIndex + params.pointerDeltaY / rowHeight),
|
|
630
|
+
0
|
|
631
|
+
),
|
|
632
|
+
Math.max(params.rowCount - 1, 0)
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
function resolveRowDragTargetOffset(params) {
|
|
636
|
+
const { rowIndex, fromIndex, targetIndex, rowHeight } = params;
|
|
637
|
+
if (rowIndex === fromIndex) return 0;
|
|
638
|
+
if (targetIndex > fromIndex && rowIndex > fromIndex && rowIndex <= targetIndex) {
|
|
639
|
+
return -rowHeight;
|
|
640
|
+
}
|
|
641
|
+
if (targetIndex < fromIndex && rowIndex >= targetIndex && rowIndex < fromIndex) {
|
|
642
|
+
return rowHeight;
|
|
643
|
+
}
|
|
644
|
+
return 0;
|
|
645
|
+
}
|
|
646
|
+
function reorderGanttTasks(tasks, fromIndex, targetIndex) {
|
|
647
|
+
if (fromIndex === targetIndex || fromIndex < 0 || fromIndex >= tasks.length || targetIndex < 0 || targetIndex >= tasks.length) {
|
|
648
|
+
return tasks.slice();
|
|
649
|
+
}
|
|
650
|
+
const reordered = tasks.slice();
|
|
651
|
+
const [task] = reordered.splice(fromIndex, 1);
|
|
652
|
+
if (task) reordered.splice(targetIndex, 0, task);
|
|
653
|
+
return reordered;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/interaction/wheel-scroll.ts
|
|
657
|
+
var WHEEL_DELTA_MODE_PIXEL = 0;
|
|
658
|
+
var WHEEL_DELTA_MODE_LINE = 1;
|
|
659
|
+
var WHEEL_DELTA_MODE_PAGE = 2;
|
|
660
|
+
function normalizeWheelDeltaToPixels(params) {
|
|
661
|
+
const delta = Number.isFinite(params.delta) ? params.delta : 0;
|
|
662
|
+
const lineHeight = Math.max(params.lineHeight, 1);
|
|
663
|
+
const pageSize = Math.max(params.pageSize, 1);
|
|
664
|
+
if (params.deltaMode === WHEEL_DELTA_MODE_LINE) return delta * lineHeight;
|
|
665
|
+
if (params.deltaMode === WHEEL_DELTA_MODE_PAGE) return delta * pageSize;
|
|
666
|
+
return delta;
|
|
667
|
+
}
|
|
668
|
+
function softenWheelDeltaPixels(params) {
|
|
669
|
+
const { delta } = params;
|
|
670
|
+
if (delta === 0) return 0;
|
|
671
|
+
const absoluteDelta = Math.abs(delta);
|
|
672
|
+
const lineHeight = Math.max(params.lineHeight, 1);
|
|
673
|
+
if (params.deltaMode === WHEEL_DELTA_MODE_PIXEL && absoluteDelta >= 80 && absoluteDelta <= 160) {
|
|
674
|
+
return delta * 0.42;
|
|
675
|
+
}
|
|
676
|
+
if (params.deltaMode === WHEEL_DELTA_MODE_LINE) {
|
|
677
|
+
const maxStep = Math.max(lineHeight * 0.75, 24);
|
|
678
|
+
if (absoluteDelta <= maxStep) return delta * 0.72;
|
|
679
|
+
return Math.sign(delta) * (maxStep + (absoluteDelta - maxStep) * 0.35);
|
|
680
|
+
}
|
|
681
|
+
const softThreshold = Math.max(lineHeight * 1.25, 48);
|
|
682
|
+
if (absoluteDelta <= softThreshold) return delta;
|
|
683
|
+
return Math.sign(delta) * (softThreshold + (absoluteDelta - softThreshold) * 0.4);
|
|
684
|
+
}
|
|
685
|
+
function resolveWheelScrollDelta(params) {
|
|
686
|
+
const rawX = softenWheelDeltaPixels({
|
|
687
|
+
delta: normalizeWheelDeltaToPixels({
|
|
688
|
+
delta: params.deltaX,
|
|
689
|
+
deltaMode: params.deltaMode,
|
|
690
|
+
lineHeight: params.lineHeight,
|
|
691
|
+
pageSize: params.pageWidth
|
|
692
|
+
}),
|
|
693
|
+
deltaMode: params.deltaMode,
|
|
694
|
+
lineHeight: params.lineHeight
|
|
695
|
+
});
|
|
696
|
+
const rawY = softenWheelDeltaPixels({
|
|
697
|
+
delta: normalizeWheelDeltaToPixels({
|
|
698
|
+
delta: params.deltaY,
|
|
699
|
+
deltaMode: params.deltaMode,
|
|
700
|
+
lineHeight: params.lineHeight,
|
|
701
|
+
pageSize: params.pageHeight
|
|
702
|
+
}),
|
|
703
|
+
deltaMode: params.deltaMode,
|
|
704
|
+
lineHeight: params.lineHeight
|
|
705
|
+
});
|
|
706
|
+
return params.shiftKey ? { left: rawX + rawY, top: 0 } : { left: rawX, top: rawY };
|
|
707
|
+
}
|
|
708
|
+
function shouldApplyWheelDeltaImmediately(params) {
|
|
709
|
+
if (params.deltaMode !== WHEEL_DELTA_MODE_PIXEL) return false;
|
|
710
|
+
return Math.abs(params.deltaX) < 40 && Math.abs(params.deltaY) < 40;
|
|
711
|
+
}
|
|
712
|
+
function advanceWheelScrollSmoothing(params) {
|
|
713
|
+
const timeConstant = Math.max(params.timeConstantMs ?? 92, 1);
|
|
714
|
+
const deltaMs = Math.max(Math.min(params.deltaMs, 64), 0);
|
|
715
|
+
const alpha = 1 - Math.exp(-deltaMs / timeConstant);
|
|
716
|
+
const nextLeft = params.current.left + (params.target.left - params.current.left) * alpha;
|
|
717
|
+
const nextTop = params.current.top + (params.target.top - params.current.top) * alpha;
|
|
718
|
+
const done = Math.abs(params.target.left - nextLeft) < 0.35 && Math.abs(params.target.top - nextTop) < 0.35;
|
|
719
|
+
return {
|
|
720
|
+
nextScroll: done ? { left: params.target.left, top: params.target.top } : { left: nextLeft, top: nextTop },
|
|
721
|
+
done
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/engine/wheel-scroll-controller.ts
|
|
726
|
+
var GanttWheelScrollController = class {
|
|
727
|
+
constructor(host) {
|
|
728
|
+
__publicField(this, "host", host);
|
|
729
|
+
}
|
|
730
|
+
/** 消费滚轮输入,并根据设备特征选择即时或平滑滚动。 */
|
|
731
|
+
handleWheel(event) {
|
|
732
|
+
const { runtime } = this.host;
|
|
733
|
+
if (!runtime.layout || !runtime.viewport) return;
|
|
734
|
+
event.preventDefault();
|
|
735
|
+
const lineHeight = this.host.getOptions().rowHeight ?? 36;
|
|
736
|
+
const delta = resolveWheelScrollDelta({
|
|
737
|
+
deltaX: event.deltaX,
|
|
738
|
+
deltaY: event.deltaY,
|
|
739
|
+
deltaMode: event.deltaMode,
|
|
740
|
+
shiftKey: event.shiftKey,
|
|
741
|
+
lineHeight,
|
|
742
|
+
pageWidth: runtime.viewport.timelineViewportWidth,
|
|
743
|
+
pageHeight: runtime.viewport.bodyViewportHeight
|
|
744
|
+
});
|
|
745
|
+
if (delta.left === 0 && delta.top === 0) return;
|
|
746
|
+
const base = runtime.wheelScrollTarget ?? runtime.scroll;
|
|
747
|
+
const target = clampGanttScroll(
|
|
748
|
+
{ left: base.left + delta.left, top: base.top + delta.top },
|
|
749
|
+
runtime.viewport.bounds
|
|
750
|
+
);
|
|
751
|
+
if (shouldApplyWheelDeltaImmediately({
|
|
752
|
+
deltaX: event.deltaX,
|
|
753
|
+
deltaY: event.deltaY,
|
|
754
|
+
deltaMode: event.deltaMode
|
|
755
|
+
})) {
|
|
756
|
+
this.stop();
|
|
757
|
+
if (target.left !== runtime.scroll.left || target.top !== runtime.scroll.top) {
|
|
758
|
+
runtime.scroll = target;
|
|
759
|
+
this.host.render();
|
|
760
|
+
}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (target.left === runtime.scroll.left && target.top === runtime.scroll.top && runtime.wheelScrollFrameId == null) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
runtime.wheelScrollTarget = target;
|
|
767
|
+
if (runtime.wheelScrollFrameId == null) {
|
|
768
|
+
runtime.wheelScrollLastTime = performance.now();
|
|
769
|
+
this.scheduleNextFrame();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/** 停止尚未完成的滚轮缓动。 */
|
|
773
|
+
stop() {
|
|
774
|
+
const { runtime } = this.host;
|
|
775
|
+
if (runtime.wheelScrollFrameId != null) {
|
|
776
|
+
cancelAnimationFrame(runtime.wheelScrollFrameId);
|
|
777
|
+
}
|
|
778
|
+
runtime.wheelScrollFrameId = null;
|
|
779
|
+
runtime.wheelScrollTarget = null;
|
|
780
|
+
runtime.wheelScrollLastTime = 0;
|
|
781
|
+
}
|
|
782
|
+
scheduleNextFrame() {
|
|
783
|
+
this.host.runtime.wheelScrollFrameId = requestAnimationFrame(() => {
|
|
784
|
+
this.advanceFrame();
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
advanceFrame() {
|
|
788
|
+
const { runtime } = this.host;
|
|
789
|
+
runtime.wheelScrollFrameId = null;
|
|
790
|
+
const pendingTarget = runtime.wheelScrollTarget;
|
|
791
|
+
if (!pendingTarget || !runtime.viewport) return;
|
|
792
|
+
const target = clampGanttScroll(pendingTarget, runtime.viewport.bounds);
|
|
793
|
+
runtime.wheelScrollTarget = target;
|
|
794
|
+
const now = performance.now();
|
|
795
|
+
const { nextScroll, done } = advanceWheelScrollSmoothing({
|
|
796
|
+
current: runtime.scroll,
|
|
797
|
+
target,
|
|
798
|
+
deltaMs: now - runtime.wheelScrollLastTime
|
|
799
|
+
});
|
|
800
|
+
runtime.wheelScrollLastTime = now;
|
|
801
|
+
runtime.scroll = nextScroll;
|
|
802
|
+
this.host.render();
|
|
803
|
+
if (done) {
|
|
804
|
+
runtime.wheelScrollTarget = null;
|
|
805
|
+
runtime.wheelScrollLastTime = 0;
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
this.scheduleNextFrame();
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// src/engine/event-controller.ts
|
|
813
|
+
var GanttEventController = class {
|
|
814
|
+
constructor(host) {
|
|
815
|
+
__publicField(this, "host", host);
|
|
816
|
+
__publicField(this, "wheelScrollController");
|
|
817
|
+
__publicField(this, "rowDragFrameId", null);
|
|
818
|
+
__publicField(this, "handlePointerDown", (event) => {
|
|
819
|
+
this.wheelScrollController.stop();
|
|
820
|
+
const { runtime } = this.host;
|
|
821
|
+
if (!runtime.layout || !runtime.viewport) return;
|
|
822
|
+
const point = this.point(event);
|
|
823
|
+
this.host.canvas.focus({ preventScroll: true });
|
|
824
|
+
const scrollbarHit = hitTestGanttScrollbar(
|
|
825
|
+
point.x,
|
|
826
|
+
point.y,
|
|
827
|
+
runtime.scrollbars
|
|
828
|
+
);
|
|
829
|
+
if (scrollbarHit) {
|
|
830
|
+
this.handleScrollbarPointerDown(
|
|
831
|
+
event,
|
|
832
|
+
scrollbarHit.axis,
|
|
833
|
+
scrollbarHit.role,
|
|
834
|
+
scrollbarHit.axis === "horizontal" ? point.x : point.y
|
|
835
|
+
);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (this.isTaskColumnResizeHandle(point.x, point.y)) {
|
|
839
|
+
const options2 = this.host.getOptions();
|
|
840
|
+
const startWidth = runtime.layout.taskColumnWidth;
|
|
841
|
+
runtime.columnResize = {
|
|
842
|
+
startX: point.x,
|
|
843
|
+
startWidth,
|
|
844
|
+
minWidth: Math.max(options2.taskColumnMinWidth ?? 120, 48),
|
|
845
|
+
maxWidth: Math.max(
|
|
846
|
+
Math.min(
|
|
847
|
+
options2.taskColumnMaxWidth ?? 480,
|
|
848
|
+
runtime.viewport.width - 80
|
|
849
|
+
),
|
|
850
|
+
options2.taskColumnMinWidth ?? 120
|
|
851
|
+
)
|
|
852
|
+
};
|
|
853
|
+
this.host.canvas.style.cursor = "col-resize";
|
|
854
|
+
this.host.canvas.setPointerCapture(event.pointerId);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (this.startRowDrag(event, point.x, point.y)) return;
|
|
858
|
+
if (point.x < runtime.layout.taskColumnWidth || point.x >= runtime.viewport.contentRight || point.y < runtime.layout.headerHeight || point.y >= runtime.viewport.contentBottom) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const contentX = point.x + runtime.scroll.left;
|
|
862
|
+
const contentY = point.y + runtime.scroll.top;
|
|
863
|
+
const options = this.host.getOptions();
|
|
864
|
+
if (options.editable === true) {
|
|
865
|
+
const anchor = hitTestTaskDependencyAnchor({
|
|
866
|
+
x: contentX,
|
|
867
|
+
y: contentY,
|
|
868
|
+
layout: runtime.layout,
|
|
869
|
+
selectedTaskId: runtime.selectedId
|
|
870
|
+
});
|
|
871
|
+
if (anchor) {
|
|
872
|
+
runtime.dependencyDrag = {
|
|
873
|
+
sourceTaskId: anchor.task.id,
|
|
874
|
+
sourceSide: anchor.side,
|
|
875
|
+
sourceX: anchor.x,
|
|
876
|
+
sourceY: anchor.y,
|
|
877
|
+
currentX: anchor.x,
|
|
878
|
+
currentY: anchor.y,
|
|
879
|
+
targetTaskId: null,
|
|
880
|
+
targetSide: null
|
|
881
|
+
};
|
|
882
|
+
runtime.selectedDependencyIndex = null;
|
|
883
|
+
this.host.canvas.style.cursor = "crosshair";
|
|
884
|
+
this.host.canvas.setPointerCapture(event.pointerId);
|
|
885
|
+
this.host.render();
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const hit = hitTestTaskBar({
|
|
890
|
+
x: contentX,
|
|
891
|
+
y: contentY,
|
|
892
|
+
layout: runtime.layout,
|
|
893
|
+
selectedId: runtime.selectedId,
|
|
894
|
+
resizable: options.editable === true && options.taskResizable !== false
|
|
895
|
+
});
|
|
896
|
+
if (!hit) {
|
|
897
|
+
const dependencyHit = hitTestGanttDependency({
|
|
898
|
+
x: contentX,
|
|
899
|
+
y: contentY,
|
|
900
|
+
layout: runtime.layout,
|
|
901
|
+
dependencies: options.dependencies ?? []
|
|
902
|
+
});
|
|
903
|
+
if (dependencyHit) {
|
|
904
|
+
runtime.selectedId = null;
|
|
905
|
+
runtime.selectedDependencyIndex = dependencyHit.index;
|
|
906
|
+
options.onDependencyClick?.(
|
|
907
|
+
dependencyHit.dependency,
|
|
908
|
+
dependencyHit.index
|
|
909
|
+
);
|
|
910
|
+
this.host.render();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
runtime.selectedDependencyIndex = null;
|
|
914
|
+
if (options.editable === true) {
|
|
915
|
+
this.startTaskCreate(event, contentX, contentY);
|
|
916
|
+
}
|
|
917
|
+
this.host.render();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
runtime.selectedId = hit.task.id;
|
|
921
|
+
runtime.selectedDependencyIndex = null;
|
|
922
|
+
options.onTaskClick?.(hit.task);
|
|
923
|
+
if (options.editable !== true || !hasTaskSchedule(hit.task)) {
|
|
924
|
+
this.host.render();
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
runtime.taskDrag = {
|
|
928
|
+
task: hit.task,
|
|
929
|
+
mode: hit.role === "body" ? "move" : hit.role === "resize-start" ? "resize-start" : "resize-end",
|
|
930
|
+
startX: contentX,
|
|
931
|
+
originalStart: toTimestamp(hit.task.start),
|
|
932
|
+
originalEnd: toTimestamp(hit.task.end)
|
|
933
|
+
};
|
|
934
|
+
this.host.canvas.style.cursor = hit.role === "body" ? "grabbing" : "ew-resize";
|
|
935
|
+
this.host.canvas.setPointerCapture(event.pointerId);
|
|
936
|
+
this.host.render();
|
|
937
|
+
});
|
|
938
|
+
__publicField(this, "handlePointerMove", (event) => {
|
|
939
|
+
const { runtime } = this.host;
|
|
940
|
+
const point = this.point(event);
|
|
941
|
+
if (runtime.scrollbarDrag) {
|
|
942
|
+
const drag = runtime.scrollbarDrag;
|
|
943
|
+
const coordinate = drag.axis === "horizontal" ? point.x : point.y;
|
|
944
|
+
const availableTrack = Math.max(drag.trackLength - drag.thumbLength, 1);
|
|
945
|
+
const next = (coordinate - drag.pointerOffset - drag.trackStart) / availableTrack * drag.maxScroll;
|
|
946
|
+
this.scrollAxisTo(drag.axis, next, true);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (runtime.columnResize) {
|
|
950
|
+
const drag = runtime.columnResize;
|
|
951
|
+
runtime.taskColumnWidth = Math.round(
|
|
952
|
+
Math.min(
|
|
953
|
+
Math.max(drag.startWidth + point.x - drag.startX, drag.minWidth),
|
|
954
|
+
drag.maxWidth
|
|
955
|
+
)
|
|
956
|
+
);
|
|
957
|
+
this.host.render();
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (runtime.rowDrag) {
|
|
961
|
+
this.updateRowDrag(point.y + runtime.scroll.top);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (runtime.dependencyDrag && runtime.layout) {
|
|
965
|
+
this.updateDependencyDrag(
|
|
966
|
+
point.x + runtime.scroll.left,
|
|
967
|
+
point.y + runtime.scroll.top
|
|
968
|
+
);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (runtime.taskDrag && runtime.layout) {
|
|
972
|
+
this.updateTaskDrag(point.x + runtime.scroll.left, false);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
this.updateCursor(point.x, point.y);
|
|
976
|
+
});
|
|
977
|
+
__publicField(this, "handlePointerUp", (event) => {
|
|
978
|
+
const { runtime } = this.host;
|
|
979
|
+
if (runtime.dependencyDrag) {
|
|
980
|
+
this.finishDependencyDrag();
|
|
981
|
+
runtime.dependencyDrag = null;
|
|
982
|
+
this.host.render();
|
|
983
|
+
}
|
|
984
|
+
if (runtime.rowDrag) {
|
|
985
|
+
this.finishRowDrag();
|
|
986
|
+
}
|
|
987
|
+
if (runtime.taskDrag) {
|
|
988
|
+
if (this.host.getOptions().snapToColumn === true) {
|
|
989
|
+
const point = this.point(event);
|
|
990
|
+
this.updateTaskDrag(point.x + runtime.scroll.left, true);
|
|
991
|
+
}
|
|
992
|
+
this.host.getOptions().onTaskChange?.(runtime.taskDrag.task);
|
|
993
|
+
}
|
|
994
|
+
if (runtime.columnResize && runtime.taskColumnWidth != null) {
|
|
995
|
+
this.host.getOptions().onTaskColumnResize?.(runtime.taskColumnWidth);
|
|
996
|
+
}
|
|
997
|
+
runtime.taskDrag = null;
|
|
998
|
+
runtime.dependencyDrag = null;
|
|
999
|
+
runtime.scrollbarDrag = null;
|
|
1000
|
+
runtime.columnResize = null;
|
|
1001
|
+
this.host.canvas.style.cursor = "default";
|
|
1002
|
+
if (this.host.canvas.hasPointerCapture(event.pointerId)) {
|
|
1003
|
+
this.host.canvas.releasePointerCapture(event.pointerId);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
__publicField(this, "handlePointerLeave", () => {
|
|
1007
|
+
if (!this.host.runtime.taskDrag && !this.host.runtime.dependencyDrag && !this.host.runtime.scrollbarDrag && !this.host.runtime.columnResize && !this.host.runtime.rowDrag) {
|
|
1008
|
+
this.host.canvas.style.cursor = "default";
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
__publicField(this, "handleWheel", (event) => {
|
|
1012
|
+
this.wheelScrollController.handleWheel(event);
|
|
1013
|
+
});
|
|
1014
|
+
__publicField(this, "handleKeyDown", (event) => {
|
|
1015
|
+
if (this.host.getOptions().editable !== true || event.key !== "Delete" && event.key !== "Backspace") {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const { runtime } = this.host;
|
|
1019
|
+
const options = this.host.getOptions();
|
|
1020
|
+
if (runtime.selectedDependencyIndex != null) {
|
|
1021
|
+
const dependencies = options.dependencies ?? [];
|
|
1022
|
+
if (dependencies[runtime.selectedDependencyIndex]) {
|
|
1023
|
+
event.preventDefault();
|
|
1024
|
+
const next = removeGanttDependency(
|
|
1025
|
+
dependencies,
|
|
1026
|
+
runtime.selectedDependencyIndex
|
|
1027
|
+
);
|
|
1028
|
+
options.dependencies = next;
|
|
1029
|
+
runtime.selectedDependencyIndex = null;
|
|
1030
|
+
options.onDependenciesChange?.(next);
|
|
1031
|
+
this.host.render();
|
|
1032
|
+
}
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const task = findGanttTaskById(options.tasks, runtime.selectedId);
|
|
1036
|
+
if (!task || !hasTaskSchedule(task)) return;
|
|
1037
|
+
event.preventDefault();
|
|
1038
|
+
clearTaskSchedule(task);
|
|
1039
|
+
runtime.selectedId = null;
|
|
1040
|
+
this.host.getOptions().onTaskChange?.(task);
|
|
1041
|
+
this.host.render();
|
|
1042
|
+
});
|
|
1043
|
+
this.wheelScrollController = new GanttWheelScrollController(host);
|
|
1044
|
+
}
|
|
1045
|
+
/** 停止控制器持有的异步交互。 */
|
|
1046
|
+
destroy() {
|
|
1047
|
+
this.wheelScrollController.stop();
|
|
1048
|
+
this.stopRowDragAnimation();
|
|
1049
|
+
}
|
|
1050
|
+
/** 停止尚未完成的滚轮缓动。 */
|
|
1051
|
+
stopWheelScroll() {
|
|
1052
|
+
this.wheelScrollController.stop();
|
|
1053
|
+
}
|
|
1054
|
+
handleScrollbarPointerDown(event, axis, role, coordinate) {
|
|
1055
|
+
const { runtime } = this.host;
|
|
1056
|
+
const scrollbar = runtime.scrollbars[axis];
|
|
1057
|
+
if (!scrollbar || !runtime.layout || !runtime.viewport) return;
|
|
1058
|
+
event.preventDefault();
|
|
1059
|
+
this.host.canvas.style.cursor = role === "thumb" ? "grabbing" : "pointer";
|
|
1060
|
+
if (role === "decrease-button" || role === "increase-button") {
|
|
1061
|
+
const direction = role === "decrease-button" ? -1 : 1;
|
|
1062
|
+
const step = axis === "horizontal" ? runtime.layout.dayWidth : runtime.layout.rowHeight;
|
|
1063
|
+
this.scrollAxisBy(axis, direction * step);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (role === "track") {
|
|
1067
|
+
this.jumpToTrackPosition(axis, coordinate, scrollbar);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
const trackStart = axis === "horizontal" ? scrollbar.track.x : scrollbar.track.y;
|
|
1071
|
+
const trackLength = axis === "horizontal" ? scrollbar.track.width : scrollbar.track.height;
|
|
1072
|
+
const thumbStart = axis === "horizontal" ? scrollbar.thumb.x : scrollbar.thumb.y;
|
|
1073
|
+
const thumbLength = axis === "horizontal" ? scrollbar.thumb.width : scrollbar.thumb.height;
|
|
1074
|
+
runtime.scrollbarDrag = {
|
|
1075
|
+
axis,
|
|
1076
|
+
pointerOffset: coordinate - thumbStart,
|
|
1077
|
+
trackStart,
|
|
1078
|
+
trackLength,
|
|
1079
|
+
thumbLength,
|
|
1080
|
+
maxScroll: axis === "horizontal" ? runtime.viewport.bounds.maxLeft : runtime.viewport.bounds.maxTop
|
|
1081
|
+
};
|
|
1082
|
+
this.host.canvas.setPointerCapture(event.pointerId);
|
|
1083
|
+
}
|
|
1084
|
+
jumpToTrackPosition(axis, coordinate, scrollbar) {
|
|
1085
|
+
const { viewport } = this.host.runtime;
|
|
1086
|
+
if (!viewport) return;
|
|
1087
|
+
const trackStart = axis === "horizontal" ? scrollbar.track.x : scrollbar.track.y;
|
|
1088
|
+
const trackLength = axis === "horizontal" ? scrollbar.track.width : scrollbar.track.height;
|
|
1089
|
+
const thumbLength = axis === "horizontal" ? scrollbar.thumb.width : scrollbar.thumb.height;
|
|
1090
|
+
const maxScroll = axis === "horizontal" ? viewport.bounds.maxLeft : viewport.bounds.maxTop;
|
|
1091
|
+
const ratio = (coordinate - trackStart - thumbLength / 2) / Math.max(trackLength - thumbLength, 1);
|
|
1092
|
+
this.scrollAxisTo(axis, Math.min(Math.max(ratio, 0), 1) * maxScroll);
|
|
1093
|
+
}
|
|
1094
|
+
updateTaskDrag(contentX, snapToColumn) {
|
|
1095
|
+
const { runtime } = this.host;
|
|
1096
|
+
const drag = runtime.taskDrag;
|
|
1097
|
+
if (!drag || !runtime.layout) return;
|
|
1098
|
+
if (drag.mode === "create") {
|
|
1099
|
+
const range2 = resolveTaskCreateRange({
|
|
1100
|
+
anchorX: drag.startX,
|
|
1101
|
+
currentX: contentX,
|
|
1102
|
+
layout: runtime.layout
|
|
1103
|
+
});
|
|
1104
|
+
drag.task.start = new Date(range2.start);
|
|
1105
|
+
drag.task.end = new Date(range2.end);
|
|
1106
|
+
this.host.scheduleRender();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const delta = resolveTaskTimeDelta(
|
|
1110
|
+
contentX - drag.startX,
|
|
1111
|
+
runtime.layout,
|
|
1112
|
+
snapToColumn,
|
|
1113
|
+
drag.mode === "resize-end" ? drag.originalEnd : drag.originalStart
|
|
1114
|
+
);
|
|
1115
|
+
const range = resolveTaskDragRange({
|
|
1116
|
+
mode: drag.mode,
|
|
1117
|
+
originalStart: drag.originalStart,
|
|
1118
|
+
originalEnd: drag.originalEnd,
|
|
1119
|
+
delta,
|
|
1120
|
+
minStart: runtime.layout.start,
|
|
1121
|
+
maxEnd: runtime.layout.end
|
|
1122
|
+
});
|
|
1123
|
+
drag.task.start = new Date(range.start);
|
|
1124
|
+
drag.task.end = new Date(range.end);
|
|
1125
|
+
this.host.scheduleRender();
|
|
1126
|
+
}
|
|
1127
|
+
updateDependencyDrag(contentX, contentY) {
|
|
1128
|
+
const { runtime } = this.host;
|
|
1129
|
+
const drag = runtime.dependencyDrag;
|
|
1130
|
+
if (!drag || !runtime.layout) return;
|
|
1131
|
+
const target = resolveDependencyDropTarget({
|
|
1132
|
+
x: contentX,
|
|
1133
|
+
y: contentY,
|
|
1134
|
+
layout: runtime.layout,
|
|
1135
|
+
sourceTaskId: drag.sourceTaskId,
|
|
1136
|
+
sourceSide: drag.sourceSide
|
|
1137
|
+
});
|
|
1138
|
+
drag.currentX = target?.x ?? contentX;
|
|
1139
|
+
drag.currentY = target?.y ?? contentY;
|
|
1140
|
+
drag.targetTaskId = target?.task.id ?? null;
|
|
1141
|
+
drag.targetSide = target?.side ?? null;
|
|
1142
|
+
this.host.scheduleRender();
|
|
1143
|
+
}
|
|
1144
|
+
finishDependencyDrag() {
|
|
1145
|
+
const { runtime } = this.host;
|
|
1146
|
+
const drag = runtime.dependencyDrag;
|
|
1147
|
+
if (!drag || !drag.targetTaskId || !drag.targetSide) return;
|
|
1148
|
+
const type = resolveDependencyType(drag.sourceSide, drag.targetSide);
|
|
1149
|
+
if (!type) return;
|
|
1150
|
+
const options = this.host.getOptions();
|
|
1151
|
+
const dependency = {
|
|
1152
|
+
from: drag.sourceTaskId,
|
|
1153
|
+
to: drag.targetTaskId,
|
|
1154
|
+
type
|
|
1155
|
+
};
|
|
1156
|
+
const result = upsertGanttDependency(
|
|
1157
|
+
options.dependencies ?? [],
|
|
1158
|
+
dependency
|
|
1159
|
+
);
|
|
1160
|
+
options.dependencies = result.dependencies;
|
|
1161
|
+
runtime.selectedId = null;
|
|
1162
|
+
runtime.selectedDependencyIndex = result.index;
|
|
1163
|
+
options.onDependenciesChange?.(result.dependencies);
|
|
1164
|
+
}
|
|
1165
|
+
startTaskCreate(event, contentX, contentY) {
|
|
1166
|
+
const { runtime } = this.host;
|
|
1167
|
+
if (!runtime.layout) return false;
|
|
1168
|
+
const row = runtime.layout.rows.find(
|
|
1169
|
+
(item) => contentY >= item.y && contentY < item.y + runtime.layout.rowHeight
|
|
1170
|
+
);
|
|
1171
|
+
const task = row?.tasks.find((item) => !hasTaskSchedule(item));
|
|
1172
|
+
if (!row || !task) return false;
|
|
1173
|
+
const range = resolveTaskCreateRange({
|
|
1174
|
+
anchorX: contentX,
|
|
1175
|
+
currentX: contentX,
|
|
1176
|
+
layout: runtime.layout
|
|
1177
|
+
});
|
|
1178
|
+
task.start = new Date(range.start);
|
|
1179
|
+
task.end = new Date(range.end);
|
|
1180
|
+
runtime.selectedId = task.id;
|
|
1181
|
+
runtime.taskDrag = {
|
|
1182
|
+
task,
|
|
1183
|
+
mode: "create",
|
|
1184
|
+
startX: contentX,
|
|
1185
|
+
originalStart: range.start,
|
|
1186
|
+
originalEnd: range.end
|
|
1187
|
+
};
|
|
1188
|
+
this.host.canvas.style.cursor = "ew-resize";
|
|
1189
|
+
this.host.canvas.setPointerCapture(event.pointerId);
|
|
1190
|
+
this.host.getOptions().onTaskClick?.(task);
|
|
1191
|
+
this.host.render();
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
startRowDrag(event, pointerX, pointerY) {
|
|
1195
|
+
const { runtime } = this.host;
|
|
1196
|
+
if (this.host.getOptions().editable !== true || this.host.getOptions().rowDraggable !== true || !runtime.layout || !runtime.viewport || pointerX < 0 || pointerX >= runtime.layout.taskColumnWidth || pointerY < runtime.layout.headerHeight || pointerY >= runtime.viewport.contentBottom) {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
const contentY = pointerY + runtime.scroll.top;
|
|
1200
|
+
const row = runtime.layout.rows.find(
|
|
1201
|
+
(item) => contentY >= item.y && contentY < item.y + runtime.layout.rowHeight
|
|
1202
|
+
);
|
|
1203
|
+
if (!row) return false;
|
|
1204
|
+
const multiTask = this.host.getOptions().multiTask === true;
|
|
1205
|
+
runtime.selectedId = multiTask ? null : row.task.id;
|
|
1206
|
+
runtime.rowDrag = {
|
|
1207
|
+
phase: "potential",
|
|
1208
|
+
pointerId: event.pointerId,
|
|
1209
|
+
taskId: row.task.id,
|
|
1210
|
+
fromIndex: row.index,
|
|
1211
|
+
targetIndex: row.index,
|
|
1212
|
+
startY: contentY,
|
|
1213
|
+
currentY: contentY,
|
|
1214
|
+
offsets: /* @__PURE__ */ new Map()
|
|
1215
|
+
};
|
|
1216
|
+
this.host.canvas.style.cursor = "grab";
|
|
1217
|
+
this.host.canvas.setPointerCapture(event.pointerId);
|
|
1218
|
+
if (!multiTask) this.host.getOptions().onTaskClick?.(row.task);
|
|
1219
|
+
this.host.render();
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
updateRowDrag(contentY) {
|
|
1223
|
+
const { runtime } = this.host;
|
|
1224
|
+
const drag = runtime.rowDrag;
|
|
1225
|
+
if (!drag || !runtime.layout) return;
|
|
1226
|
+
const deltaY = contentY - drag.startY;
|
|
1227
|
+
if (drag.phase === "potential") {
|
|
1228
|
+
if (Math.abs(deltaY) < 5) return;
|
|
1229
|
+
drag.phase = "dragging";
|
|
1230
|
+
this.host.canvas.style.cursor = "grabbing";
|
|
1231
|
+
}
|
|
1232
|
+
drag.currentY = contentY;
|
|
1233
|
+
drag.targetIndex = resolveRowDragTargetIndex({
|
|
1234
|
+
pointerDeltaY: deltaY,
|
|
1235
|
+
fromIndex: drag.fromIndex,
|
|
1236
|
+
rowHeight: runtime.layout.rowHeight,
|
|
1237
|
+
rowCount: runtime.layout.rows.length
|
|
1238
|
+
});
|
|
1239
|
+
drag.offsets.set(drag.taskId, deltaY);
|
|
1240
|
+
this.host.scheduleRender();
|
|
1241
|
+
this.scheduleRowDragAnimation();
|
|
1242
|
+
}
|
|
1243
|
+
finishRowDrag() {
|
|
1244
|
+
const drag = this.host.runtime.rowDrag;
|
|
1245
|
+
if (!drag) return;
|
|
1246
|
+
this.stopRowDragAnimation();
|
|
1247
|
+
if (drag.phase === "dragging" && drag.fromIndex !== drag.targetIndex) {
|
|
1248
|
+
const options = this.host.getOptions();
|
|
1249
|
+
const tasks = reorderGanttTasks(
|
|
1250
|
+
options.tasks,
|
|
1251
|
+
drag.fromIndex,
|
|
1252
|
+
drag.targetIndex
|
|
1253
|
+
);
|
|
1254
|
+
options.tasks = tasks;
|
|
1255
|
+
options.onTaskReorder?.(tasks, drag.fromIndex, drag.targetIndex);
|
|
1256
|
+
}
|
|
1257
|
+
this.host.runtime.rowDrag = null;
|
|
1258
|
+
this.host.render();
|
|
1259
|
+
}
|
|
1260
|
+
scheduleRowDragAnimation() {
|
|
1261
|
+
if (this.rowDragFrameId != null) return;
|
|
1262
|
+
this.rowDragFrameId = requestAnimationFrame(() => {
|
|
1263
|
+
this.rowDragFrameId = null;
|
|
1264
|
+
this.advanceRowDragAnimation();
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
advanceRowDragAnimation() {
|
|
1268
|
+
const { runtime } = this.host;
|
|
1269
|
+
const drag = runtime.rowDrag;
|
|
1270
|
+
if (!drag || drag.phase !== "dragging" || !runtime.layout) return;
|
|
1271
|
+
let done = true;
|
|
1272
|
+
runtime.layout.rows.forEach((row) => {
|
|
1273
|
+
if (row.task.id === drag.taskId) return;
|
|
1274
|
+
const target = resolveRowDragTargetOffset({
|
|
1275
|
+
rowIndex: row.index,
|
|
1276
|
+
fromIndex: drag.fromIndex,
|
|
1277
|
+
targetIndex: drag.targetIndex,
|
|
1278
|
+
rowHeight: runtime.layout.rowHeight
|
|
1279
|
+
});
|
|
1280
|
+
const current = drag.offsets.get(row.task.id) ?? 0;
|
|
1281
|
+
const next = current + (target - current) * 0.28;
|
|
1282
|
+
if (Math.abs(target - next) < 0.35) {
|
|
1283
|
+
drag.offsets.set(row.task.id, target);
|
|
1284
|
+
} else {
|
|
1285
|
+
drag.offsets.set(row.task.id, next);
|
|
1286
|
+
done = false;
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
this.host.render();
|
|
1290
|
+
if (!done) this.scheduleRowDragAnimation();
|
|
1291
|
+
}
|
|
1292
|
+
stopRowDragAnimation() {
|
|
1293
|
+
if (this.rowDragFrameId != null) cancelAnimationFrame(this.rowDragFrameId);
|
|
1294
|
+
this.rowDragFrameId = null;
|
|
1295
|
+
}
|
|
1296
|
+
updateCursor(x, y) {
|
|
1297
|
+
const { runtime } = this.host;
|
|
1298
|
+
const scrollbarHit = hitTestGanttScrollbar(x, y, runtime.scrollbars);
|
|
1299
|
+
if (scrollbarHit) {
|
|
1300
|
+
this.host.canvas.style.cursor = scrollbarHit.role === "thumb" ? "grab" : "pointer";
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (this.isTaskColumnResizeHandle(x, y)) {
|
|
1304
|
+
this.host.canvas.style.cursor = "col-resize";
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (this.host.getOptions().editable === true && this.host.getOptions().rowDraggable === true && !!runtime.layout && !!runtime.viewport && x >= 0 && x < runtime.layout.taskColumnWidth && y >= runtime.layout.headerHeight && y < runtime.viewport.contentBottom) {
|
|
1308
|
+
this.host.canvas.style.cursor = "grab";
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (!runtime.layout || !runtime.viewport || x < runtime.layout.taskColumnWidth || x >= runtime.viewport.contentRight || y < runtime.layout.headerHeight || y >= runtime.viewport.contentBottom) {
|
|
1312
|
+
this.host.canvas.style.cursor = "default";
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const hit = hitTestTaskBar({
|
|
1316
|
+
x: x + runtime.scroll.left,
|
|
1317
|
+
y: y + runtime.scroll.top,
|
|
1318
|
+
layout: runtime.layout,
|
|
1319
|
+
selectedId: runtime.selectedId,
|
|
1320
|
+
resizable: this.host.getOptions().editable === true && this.host.getOptions().taskResizable !== false
|
|
1321
|
+
});
|
|
1322
|
+
if (this.host.getOptions().editable === true) {
|
|
1323
|
+
const anchor = hitTestTaskDependencyAnchor({
|
|
1324
|
+
x: x + runtime.scroll.left,
|
|
1325
|
+
y: y + runtime.scroll.top,
|
|
1326
|
+
layout: runtime.layout,
|
|
1327
|
+
selectedTaskId: runtime.selectedId
|
|
1328
|
+
});
|
|
1329
|
+
if (anchor) {
|
|
1330
|
+
this.host.canvas.style.cursor = "crosshair";
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (hit) {
|
|
1335
|
+
this.host.canvas.style.cursor = this.host.getOptions().editable !== true ? "pointer" : hit.role === "body" ? "grab" : "ew-resize";
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const dependencyHit = hitTestGanttDependency({
|
|
1339
|
+
x: x + runtime.scroll.left,
|
|
1340
|
+
y: y + runtime.scroll.top,
|
|
1341
|
+
layout: runtime.layout,
|
|
1342
|
+
dependencies: this.host.getOptions().dependencies ?? []
|
|
1343
|
+
});
|
|
1344
|
+
if (dependencyHit) {
|
|
1345
|
+
this.host.canvas.style.cursor = "pointer";
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const contentY = y + runtime.scroll.top;
|
|
1349
|
+
const row = runtime.layout.rows.find(
|
|
1350
|
+
(item) => contentY >= item.y && contentY < item.y + runtime.layout.rowHeight
|
|
1351
|
+
);
|
|
1352
|
+
this.host.canvas.style.cursor = this.host.getOptions().editable === true && !!row && row.tasks.some((task) => !hasTaskSchedule(task)) ? "crosshair" : "default";
|
|
1353
|
+
}
|
|
1354
|
+
scrollAxisBy(axis, delta) {
|
|
1355
|
+
const current = axis === "horizontal" ? this.host.runtime.scroll.left : this.host.runtime.scroll.top;
|
|
1356
|
+
this.scrollAxisTo(axis, current + delta);
|
|
1357
|
+
}
|
|
1358
|
+
isTaskColumnResizeHandle(x, y) {
|
|
1359
|
+
const { layout, viewport } = this.host.runtime;
|
|
1360
|
+
return this.host.getOptions().taskColumnResizable !== false && !!layout && !!viewport && y >= 0 && y < viewport.contentBottom && x >= layout.taskColumnWidth - 5 && x <= layout.taskColumnWidth;
|
|
1361
|
+
}
|
|
1362
|
+
scrollAxisTo(axis, value, scheduleRender = false) {
|
|
1363
|
+
if (scheduleRender) {
|
|
1364
|
+
const { runtime } = this.host;
|
|
1365
|
+
if (!runtime.viewport) return;
|
|
1366
|
+
const next = clampGanttScroll(
|
|
1367
|
+
axis === "horizontal" ? { left: value, top: runtime.scroll.top } : { left: runtime.scroll.left, top: value },
|
|
1368
|
+
runtime.viewport.bounds
|
|
1369
|
+
);
|
|
1370
|
+
if (next.left === runtime.scroll.left && next.top === runtime.scroll.top) {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
runtime.scroll = next;
|
|
1374
|
+
this.host.scheduleRender();
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
if (axis === "horizontal") {
|
|
1378
|
+
this.host.scrollTo(value, this.host.runtime.scroll.top);
|
|
1379
|
+
} else {
|
|
1380
|
+
this.host.scrollTo(this.host.runtime.scroll.left, value);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
point(event) {
|
|
1384
|
+
const rect = this.host.canvas.getBoundingClientRect();
|
|
1385
|
+
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
// src/domain/timeline-headers.ts
|
|
1390
|
+
function buildTimelineHeaderCells(layout, level) {
|
|
1391
|
+
const cells = [];
|
|
1392
|
+
for (let start = layout.start; start < layout.end; start = resolveNextHeaderStart(start, level.unit)) {
|
|
1393
|
+
const end = Math.min(resolveNextHeaderStart(start, level.unit), layout.end);
|
|
1394
|
+
cells.push({
|
|
1395
|
+
start,
|
|
1396
|
+
end,
|
|
1397
|
+
x: xForDate(start, layout),
|
|
1398
|
+
width: (end - start) / dayMs * layout.dayWidth,
|
|
1399
|
+
label: level.formatter?.(new Date(start), new Date(end)) ?? formatHeaderLabel(start, end, level.unit)
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
return cells;
|
|
1403
|
+
}
|
|
1404
|
+
function resolveNextHeaderStart(timestamp, unit) {
|
|
1405
|
+
if (unit === "day") return timestamp + dayMs;
|
|
1406
|
+
if (unit === "week") {
|
|
1407
|
+
const date2 = new Date(timestamp);
|
|
1408
|
+
const daysUntilNextMonday = 7 - (date2.getDay() + 6) % 7;
|
|
1409
|
+
return timestamp + daysUntilNextMonday * dayMs;
|
|
1410
|
+
}
|
|
1411
|
+
const date = new Date(timestamp);
|
|
1412
|
+
if (unit === "month") {
|
|
1413
|
+
date.setMonth(date.getMonth() + 1, 1);
|
|
1414
|
+
} else if (unit === "quarter") {
|
|
1415
|
+
date.setMonth((Math.floor(date.getMonth() / 3) + 1) * 3, 1);
|
|
1416
|
+
} else {
|
|
1417
|
+
date.setFullYear(date.getFullYear() + 1, 0, 1);
|
|
1418
|
+
}
|
|
1419
|
+
return date.getTime();
|
|
1420
|
+
}
|
|
1421
|
+
function formatHeaderLabel(start, end, unit) {
|
|
1422
|
+
const date = new Date(start);
|
|
1423
|
+
if (unit === "year") return `${date.getFullYear()}\u5E74`;
|
|
1424
|
+
if (unit === "quarter") {
|
|
1425
|
+
return `${date.getFullYear()} Q${Math.floor(date.getMonth() / 3) + 1}`;
|
|
1426
|
+
}
|
|
1427
|
+
if (unit === "month") {
|
|
1428
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
1429
|
+
year: "numeric",
|
|
1430
|
+
month: "long"
|
|
1431
|
+
}).format(date);
|
|
1432
|
+
}
|
|
1433
|
+
const short = (timestamp) => new Intl.DateTimeFormat("zh-CN", {
|
|
1434
|
+
month: "2-digit",
|
|
1435
|
+
day: "2-digit"
|
|
1436
|
+
}).format(new Date(timestamp));
|
|
1437
|
+
if (unit === "week") return `${short(start)} - ${short(end - dayMs)}`;
|
|
1438
|
+
return short(start);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/rendering/primitives/dependencies.ts
|
|
1442
|
+
function drawDependencies(context, layout, dependencies, theme, rowOffsets, selectedIndex) {
|
|
1443
|
+
dependencies.forEach((dependency, index) => {
|
|
1444
|
+
const route = resolveDependencyLayoutRoute(dependency, layout, rowOffsets);
|
|
1445
|
+
if (!route) return;
|
|
1446
|
+
const color = dependency.color ?? theme.milestoneColor;
|
|
1447
|
+
context.strokeStyle = color;
|
|
1448
|
+
context.fillStyle = color;
|
|
1449
|
+
const [firstPoint, ...remainingPoints] = route.points;
|
|
1450
|
+
const lastPoint = route.points[route.points.length - 1];
|
|
1451
|
+
if (!firstPoint || !lastPoint) return;
|
|
1452
|
+
context.save();
|
|
1453
|
+
context.lineJoin = "round";
|
|
1454
|
+
context.lineCap = "round";
|
|
1455
|
+
const selected = selectedIndex === index;
|
|
1456
|
+
context.lineWidth = selected ? 2.5 : 1.5;
|
|
1457
|
+
if (selected) {
|
|
1458
|
+
context.shadowColor = theme.selectionColor;
|
|
1459
|
+
context.shadowBlur = 5;
|
|
1460
|
+
}
|
|
1461
|
+
context.beginPath();
|
|
1462
|
+
context.moveTo(firstPoint.x, firstPoint.y);
|
|
1463
|
+
remainingPoints.forEach((point) => context.lineTo(point.x, point.y));
|
|
1464
|
+
context.stroke();
|
|
1465
|
+
drawDependencyArrow(context, lastPoint, route.arrowDirection);
|
|
1466
|
+
context.restore();
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
function drawDependencyArrow(context, point, direction) {
|
|
1470
|
+
const baseX = point.x + (direction === "right" ? -7 : 7);
|
|
1471
|
+
context.beginPath();
|
|
1472
|
+
context.moveTo(point.x, point.y);
|
|
1473
|
+
context.lineTo(baseX, point.y - 4.5);
|
|
1474
|
+
context.lineTo(baseX, point.y + 4.5);
|
|
1475
|
+
context.closePath();
|
|
1476
|
+
context.fill();
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/rendering/primitives/dependency-editor.ts
|
|
1480
|
+
function drawDependencyEditor(params) {
|
|
1481
|
+
const { context, layout, selectedTaskId, drag, theme } = params;
|
|
1482
|
+
const result = findGanttLayoutTask(layout, selectedTaskId);
|
|
1483
|
+
if (result) {
|
|
1484
|
+
const anchors = resolveTaskDependencyAnchors(
|
|
1485
|
+
result.task,
|
|
1486
|
+
result.row.y,
|
|
1487
|
+
layout
|
|
1488
|
+
);
|
|
1489
|
+
anchors?.forEach((anchor) => {
|
|
1490
|
+
context.save();
|
|
1491
|
+
context.strokeStyle = theme.selectionColor;
|
|
1492
|
+
context.fillStyle = theme.backgroundColor;
|
|
1493
|
+
context.lineWidth = 1.5;
|
|
1494
|
+
context.beginPath();
|
|
1495
|
+
context.moveTo(
|
|
1496
|
+
anchor.side === "start" ? anchor.x + 4 : anchor.x - 4,
|
|
1497
|
+
anchor.y
|
|
1498
|
+
);
|
|
1499
|
+
context.lineTo(
|
|
1500
|
+
anchor.side === "start" ? anchor.x + 10 : anchor.x - 10,
|
|
1501
|
+
anchor.y
|
|
1502
|
+
);
|
|
1503
|
+
context.stroke();
|
|
1504
|
+
context.beginPath();
|
|
1505
|
+
context.arc(anchor.x, anchor.y, 4, 0, Math.PI * 2);
|
|
1506
|
+
context.fill();
|
|
1507
|
+
context.stroke();
|
|
1508
|
+
context.restore();
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
if (!drag) return;
|
|
1512
|
+
context.save();
|
|
1513
|
+
context.strokeStyle = theme.milestoneColor;
|
|
1514
|
+
context.fillStyle = theme.milestoneColor;
|
|
1515
|
+
context.lineWidth = 1.5;
|
|
1516
|
+
context.setLineDash([5, 4]);
|
|
1517
|
+
const middleX = (drag.sourceX + drag.currentX) / 2;
|
|
1518
|
+
context.beginPath();
|
|
1519
|
+
context.moveTo(drag.sourceX, drag.sourceY);
|
|
1520
|
+
context.lineTo(middleX, drag.sourceY);
|
|
1521
|
+
context.lineTo(middleX, drag.currentY);
|
|
1522
|
+
context.lineTo(drag.currentX, drag.currentY);
|
|
1523
|
+
context.stroke();
|
|
1524
|
+
context.setLineDash([]);
|
|
1525
|
+
if (drag.targetTaskId && drag.targetSide) {
|
|
1526
|
+
context.beginPath();
|
|
1527
|
+
context.arc(drag.currentX, drag.currentY, 5, 0, Math.PI * 2);
|
|
1528
|
+
context.fill();
|
|
1529
|
+
}
|
|
1530
|
+
context.restore();
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/domain/timeline-columns.ts
|
|
1534
|
+
function buildTimelineColumns(layout) {
|
|
1535
|
+
const result = [];
|
|
1536
|
+
for (let start = layout.start; start < layout.end; start = resolveNextColumnStart(start, layout.scale)) {
|
|
1537
|
+
const end = Math.min(
|
|
1538
|
+
resolveNextColumnStart(start, layout.scale),
|
|
1539
|
+
layout.end
|
|
1540
|
+
);
|
|
1541
|
+
const x = xForDate(start, layout);
|
|
1542
|
+
result.push({
|
|
1543
|
+
start,
|
|
1544
|
+
end,
|
|
1545
|
+
x,
|
|
1546
|
+
width: (end - start) / dayMs * layout.dayWidth,
|
|
1547
|
+
label: formatTimelineColumnLabel(start, end, layout.scale)
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
return result;
|
|
1551
|
+
}
|
|
1552
|
+
function resolveNextColumnStart(timestamp, scale) {
|
|
1553
|
+
if (scale === "day") return timestamp + dayMs;
|
|
1554
|
+
if (scale === "week") return timestamp + 7 * dayMs;
|
|
1555
|
+
const date = new Date(timestamp);
|
|
1556
|
+
date.setMonth(date.getMonth() + 1, 1);
|
|
1557
|
+
return date.getTime();
|
|
1558
|
+
}
|
|
1559
|
+
function formatTimelineColumnLabel(start, end, scale) {
|
|
1560
|
+
const short = (timestamp) => new Intl.DateTimeFormat("zh-CN", {
|
|
1561
|
+
month: "2-digit",
|
|
1562
|
+
day: "2-digit"
|
|
1563
|
+
}).format(new Date(timestamp));
|
|
1564
|
+
if (scale === "day") return short(start);
|
|
1565
|
+
if (scale === "week") return `${short(start)} - ${short(end - dayMs)}`;
|
|
1566
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
1567
|
+
year: "numeric",
|
|
1568
|
+
month: "long"
|
|
1569
|
+
}).format(new Date(start));
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/rendering/primitives/grid.ts
|
|
1573
|
+
function drawTimelineBodyGrid(context, layout, theme, showWeekendBackground = true) {
|
|
1574
|
+
if (showWeekendBackground) {
|
|
1575
|
+
for (let timestamp = layout.start; timestamp < layout.end; timestamp += dayMs) {
|
|
1576
|
+
const x = xForDate(timestamp, layout);
|
|
1577
|
+
const date = new Date(timestamp);
|
|
1578
|
+
if (date.getDay() === 0 || date.getDay() === 6) {
|
|
1579
|
+
context.fillStyle = theme.weekendBackgroundColor;
|
|
1580
|
+
context.fillRect(
|
|
1581
|
+
x,
|
|
1582
|
+
layout.headerHeight,
|
|
1583
|
+
layout.dayWidth,
|
|
1584
|
+
layout.totalHeight - layout.headerHeight
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
buildTimelineColumns(layout).forEach(
|
|
1590
|
+
(column) => drawVerticalLine(
|
|
1591
|
+
context,
|
|
1592
|
+
column.x,
|
|
1593
|
+
layout.headerHeight,
|
|
1594
|
+
layout.totalHeight,
|
|
1595
|
+
theme.borderColor
|
|
1596
|
+
)
|
|
1597
|
+
);
|
|
1598
|
+
drawVerticalLine(
|
|
1599
|
+
context,
|
|
1600
|
+
layout.totalWidth,
|
|
1601
|
+
layout.headerHeight,
|
|
1602
|
+
layout.totalHeight,
|
|
1603
|
+
theme.borderColor
|
|
1604
|
+
);
|
|
1605
|
+
drawRowLines(
|
|
1606
|
+
context,
|
|
1607
|
+
layout,
|
|
1608
|
+
layout.taskColumnWidth,
|
|
1609
|
+
layout.totalWidth,
|
|
1610
|
+
theme
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
function drawTaskColumnGrid(context, layout, theme) {
|
|
1614
|
+
drawRowLines(context, layout, 0, layout.taskColumnWidth, theme);
|
|
1615
|
+
}
|
|
1616
|
+
function drawVerticalLine(context, x, top, bottom, color) {
|
|
1617
|
+
context.strokeStyle = color;
|
|
1618
|
+
context.lineWidth = 1;
|
|
1619
|
+
context.beginPath();
|
|
1620
|
+
context.moveTo(Math.round(x) + 0.5, top);
|
|
1621
|
+
context.lineTo(Math.round(x) + 0.5, bottom);
|
|
1622
|
+
context.stroke();
|
|
1623
|
+
}
|
|
1624
|
+
function drawHorizontalLine(context, y, left, right, color) {
|
|
1625
|
+
context.strokeStyle = color;
|
|
1626
|
+
context.lineWidth = 1;
|
|
1627
|
+
context.beginPath();
|
|
1628
|
+
context.moveTo(left, Math.round(y) + 0.5);
|
|
1629
|
+
context.lineTo(right, Math.round(y) + 0.5);
|
|
1630
|
+
context.stroke();
|
|
1631
|
+
}
|
|
1632
|
+
function drawRowLines(context, layout, left, right, theme) {
|
|
1633
|
+
layout.rows.forEach(
|
|
1634
|
+
({ y }) => drawHorizontalLine(context, y, left, right, theme.borderColor)
|
|
1635
|
+
);
|
|
1636
|
+
drawHorizontalLine(
|
|
1637
|
+
context,
|
|
1638
|
+
layout.totalHeight,
|
|
1639
|
+
left,
|
|
1640
|
+
right,
|
|
1641
|
+
theme.borderColor
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/rendering/primitives/scrollbars.ts
|
|
1646
|
+
function drawGanttScrollbars(params) {
|
|
1647
|
+
const { context, viewport, layout, theme, headerHeight } = params;
|
|
1648
|
+
context.save();
|
|
1649
|
+
if (viewport.hasHorizontalScrollbar) {
|
|
1650
|
+
context.fillStyle = theme.backgroundColor;
|
|
1651
|
+
context.fillRect(
|
|
1652
|
+
0,
|
|
1653
|
+
viewport.contentBottom,
|
|
1654
|
+
viewport.width,
|
|
1655
|
+
viewport.height - viewport.contentBottom
|
|
1656
|
+
);
|
|
1657
|
+
drawHorizontalLine2(
|
|
1658
|
+
context,
|
|
1659
|
+
viewport.contentBottom,
|
|
1660
|
+
0,
|
|
1661
|
+
viewport.width,
|
|
1662
|
+
theme.borderColor
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
if (viewport.hasVerticalScrollbar) {
|
|
1666
|
+
context.fillStyle = theme.headerBackgroundColor;
|
|
1667
|
+
context.fillRect(
|
|
1668
|
+
viewport.contentRight,
|
|
1669
|
+
0,
|
|
1670
|
+
viewport.width - viewport.contentRight,
|
|
1671
|
+
headerHeight
|
|
1672
|
+
);
|
|
1673
|
+
context.fillStyle = theme.backgroundColor;
|
|
1674
|
+
context.fillRect(
|
|
1675
|
+
viewport.contentRight,
|
|
1676
|
+
headerHeight,
|
|
1677
|
+
viewport.width - viewport.contentRight,
|
|
1678
|
+
viewport.contentBottom - headerHeight
|
|
1679
|
+
);
|
|
1680
|
+
drawVerticalLine2(
|
|
1681
|
+
context,
|
|
1682
|
+
viewport.contentRight,
|
|
1683
|
+
0,
|
|
1684
|
+
viewport.height,
|
|
1685
|
+
theme.borderColor
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
if (layout.horizontal) {
|
|
1689
|
+
drawAxisScrollbar(context, layout.horizontal, "horizontal", theme);
|
|
1690
|
+
}
|
|
1691
|
+
if (layout.vertical) {
|
|
1692
|
+
drawAxisScrollbar(context, layout.vertical, "vertical", theme);
|
|
1693
|
+
}
|
|
1694
|
+
context.restore();
|
|
1695
|
+
}
|
|
1696
|
+
function drawAxisScrollbar(context, layout, axis, theme) {
|
|
1697
|
+
drawButton(context, layout.decreaseButton, axis, "decrease");
|
|
1698
|
+
drawButton(context, layout.increaseButton, axis, "increase");
|
|
1699
|
+
context.fillStyle = "rgba(100, 116, 139, 0.8)";
|
|
1700
|
+
context.beginPath();
|
|
1701
|
+
context.roundRect(
|
|
1702
|
+
layout.thumb.x,
|
|
1703
|
+
layout.thumb.y,
|
|
1704
|
+
layout.thumb.width,
|
|
1705
|
+
layout.thumb.height,
|
|
1706
|
+
Math.min(layout.thumb.width, layout.thumb.height) / 2
|
|
1707
|
+
);
|
|
1708
|
+
context.fill();
|
|
1709
|
+
if (axis === "horizontal") {
|
|
1710
|
+
drawHorizontalLine2(
|
|
1711
|
+
context,
|
|
1712
|
+
layout.track.y,
|
|
1713
|
+
layout.track.x,
|
|
1714
|
+
layout.track.x + layout.track.width,
|
|
1715
|
+
theme.borderColor
|
|
1716
|
+
);
|
|
1717
|
+
} else {
|
|
1718
|
+
drawVerticalLine2(
|
|
1719
|
+
context,
|
|
1720
|
+
layout.track.x,
|
|
1721
|
+
layout.track.y,
|
|
1722
|
+
layout.track.y + layout.track.height,
|
|
1723
|
+
theme.borderColor
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
function drawButton(context, rect, axis, direction) {
|
|
1728
|
+
context.save();
|
|
1729
|
+
context.fillStyle = "rgba(71, 85, 105, 0.88)";
|
|
1730
|
+
context.beginPath();
|
|
1731
|
+
const centerX = rect.x + rect.width / 2;
|
|
1732
|
+
const centerY = rect.y + rect.height / 2;
|
|
1733
|
+
const iconSize = Math.min(rect.width, rect.height) * 0.64;
|
|
1734
|
+
const directionOffset = iconSize * 0.4;
|
|
1735
|
+
const crossAxisOffset = iconSize / 2;
|
|
1736
|
+
if (axis === "horizontal") {
|
|
1737
|
+
const tipX = centerX + (direction === "decrease" ? -1 : 1) * directionOffset;
|
|
1738
|
+
const tailX = centerX + (direction === "decrease" ? 1 : -1) * directionOffset;
|
|
1739
|
+
context.moveTo(tipX, centerY);
|
|
1740
|
+
context.lineTo(tailX, centerY - crossAxisOffset);
|
|
1741
|
+
context.lineTo(tailX, centerY + crossAxisOffset);
|
|
1742
|
+
} else {
|
|
1743
|
+
const tipY = centerY + (direction === "decrease" ? -1 : 1) * directionOffset;
|
|
1744
|
+
const tailY = centerY + (direction === "decrease" ? 1 : -1) * directionOffset;
|
|
1745
|
+
context.moveTo(centerX, tipY);
|
|
1746
|
+
context.lineTo(centerX - crossAxisOffset, tailY);
|
|
1747
|
+
context.lineTo(centerX + crossAxisOffset, tailY);
|
|
1748
|
+
}
|
|
1749
|
+
context.closePath();
|
|
1750
|
+
context.fill();
|
|
1751
|
+
context.restore();
|
|
1752
|
+
}
|
|
1753
|
+
function drawHorizontalLine2(context, y, left, right, color) {
|
|
1754
|
+
context.strokeStyle = color;
|
|
1755
|
+
context.lineWidth = 1;
|
|
1756
|
+
context.beginPath();
|
|
1757
|
+
context.moveTo(left, Math.round(y) + 0.5);
|
|
1758
|
+
context.lineTo(right, Math.round(y) + 0.5);
|
|
1759
|
+
context.stroke();
|
|
1760
|
+
}
|
|
1761
|
+
function drawVerticalLine2(context, x, top, bottom, color) {
|
|
1762
|
+
context.strokeStyle = color;
|
|
1763
|
+
context.lineWidth = 1;
|
|
1764
|
+
context.beginPath();
|
|
1765
|
+
context.moveTo(Math.round(x) + 0.5, top);
|
|
1766
|
+
context.lineTo(Math.round(x) + 0.5, bottom);
|
|
1767
|
+
context.stroke();
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/domain/task-content.ts
|
|
1771
|
+
function resolveGanttTaskContentStyle(style, fallback) {
|
|
1772
|
+
return {
|
|
1773
|
+
color: style?.color ?? fallback.color,
|
|
1774
|
+
fontSize: style?.fontSize ?? fallback.fontSize,
|
|
1775
|
+
fontWeight: style?.fontWeight ?? fallback.fontWeight,
|
|
1776
|
+
fontFamily: style?.fontFamily ?? fallback.fontFamily,
|
|
1777
|
+
textAlign: style?.textAlign ?? fallback.textAlign
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function normalizeGanttTaskContent(result) {
|
|
1781
|
+
return result == null ? null : String(result);
|
|
1782
|
+
}
|
|
1783
|
+
function ellipsizeGanttTaskText(context, text, maxWidth) {
|
|
1784
|
+
if (maxWidth <= 0) return "";
|
|
1785
|
+
if (context.measureText(text).width <= maxWidth) return text;
|
|
1786
|
+
const ellipsis = "\u2026";
|
|
1787
|
+
if (context.measureText(ellipsis).width > maxWidth) return "";
|
|
1788
|
+
let left = 0;
|
|
1789
|
+
let right = text.length;
|
|
1790
|
+
while (left < right) {
|
|
1791
|
+
const middle = Math.ceil((left + right) / 2);
|
|
1792
|
+
if (context.measureText(`${text.slice(0, middle)}${ellipsis}`).width <= maxWidth) {
|
|
1793
|
+
left = middle;
|
|
1794
|
+
} else {
|
|
1795
|
+
right = middle - 1;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return `${text.slice(0, left)}${ellipsis}`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/rendering/primitives/task-bars.ts
|
|
1802
|
+
var HANDLE_WIDTH = 6;
|
|
1803
|
+
function drawTaskBars(params) {
|
|
1804
|
+
const { context, layout, selectedId, theme, resizable } = params;
|
|
1805
|
+
layout.rows.forEach((row) => {
|
|
1806
|
+
const isDragging = row.task.id === params.draggedRowId;
|
|
1807
|
+
const visualY = row.y + (params.rowOffsets?.get(row.task.id) ?? 0);
|
|
1808
|
+
row.tasks.forEach((task) => {
|
|
1809
|
+
if (!hasTaskSchedule(task)) return;
|
|
1810
|
+
const rect = resolveTaskBarRect(task, visualY, layout);
|
|
1811
|
+
if (isDragging) {
|
|
1812
|
+
context.save();
|
|
1813
|
+
context.globalAlpha = 0.82;
|
|
1814
|
+
context.shadowColor = "rgba(31, 35, 41, 0.2)";
|
|
1815
|
+
context.shadowBlur = 8;
|
|
1816
|
+
context.shadowOffsetY = 3;
|
|
1817
|
+
}
|
|
1818
|
+
context.fillStyle = task.color ?? theme.taskColor;
|
|
1819
|
+
context.beginPath();
|
|
1820
|
+
context.roundRect(rect.x, rect.y, rect.width, rect.height, 4);
|
|
1821
|
+
context.fill();
|
|
1822
|
+
if ((task.progress ?? 0) > 0) {
|
|
1823
|
+
context.fillStyle = task.progressColor ?? theme.taskProgressColor;
|
|
1824
|
+
context.beginPath();
|
|
1825
|
+
context.roundRect(
|
|
1826
|
+
rect.x,
|
|
1827
|
+
rect.y,
|
|
1828
|
+
rect.width * Math.min(100, Math.max(0, task.progress ?? 0)) / 100,
|
|
1829
|
+
rect.height,
|
|
1830
|
+
4
|
|
1831
|
+
);
|
|
1832
|
+
context.fill();
|
|
1833
|
+
}
|
|
1834
|
+
const content = task.render ? normalizeGanttTaskContent(task.render(task, row.index)) : null;
|
|
1835
|
+
if (content != null) {
|
|
1836
|
+
drawTaskBarContent(context, rect, content, task.style, theme);
|
|
1837
|
+
}
|
|
1838
|
+
if (isDragging) context.restore();
|
|
1839
|
+
if (selectedId !== task.id) return;
|
|
1840
|
+
context.strokeStyle = theme.selectionColor;
|
|
1841
|
+
context.lineWidth = 2;
|
|
1842
|
+
context.strokeRect(
|
|
1843
|
+
rect.x - 1,
|
|
1844
|
+
rect.y - 1,
|
|
1845
|
+
rect.width + 2,
|
|
1846
|
+
rect.height + 2
|
|
1847
|
+
);
|
|
1848
|
+
context.lineWidth = 1;
|
|
1849
|
+
if (!resizable) return;
|
|
1850
|
+
drawResizeHandle(context, rect.x, rect.y, rect.height, theme);
|
|
1851
|
+
drawResizeHandle(
|
|
1852
|
+
context,
|
|
1853
|
+
rect.x + rect.width,
|
|
1854
|
+
rect.y,
|
|
1855
|
+
rect.height,
|
|
1856
|
+
theme
|
|
1857
|
+
);
|
|
1858
|
+
});
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
function drawTaskBarContent(context, rect, content, taskStyle, theme) {
|
|
1862
|
+
if (!content || rect.width <= 0) return;
|
|
1863
|
+
const horizontalPadding = 8;
|
|
1864
|
+
const contentWidth = Math.max(rect.width - horizontalPadding * 2, 0);
|
|
1865
|
+
if (contentWidth <= 0) return;
|
|
1866
|
+
context.save();
|
|
1867
|
+
context.beginPath();
|
|
1868
|
+
context.roundRect(rect.x, rect.y, rect.width, rect.height, 4);
|
|
1869
|
+
context.clip();
|
|
1870
|
+
context.textBaseline = "middle";
|
|
1871
|
+
const style = resolveGanttTaskContentStyle(taskStyle, {
|
|
1872
|
+
color: theme.backgroundColor,
|
|
1873
|
+
fontSize: Math.min(theme.fontSize, rect.height - 6),
|
|
1874
|
+
fontWeight: 500,
|
|
1875
|
+
fontFamily: theme.fontFamily,
|
|
1876
|
+
textAlign: "left"
|
|
1877
|
+
});
|
|
1878
|
+
context.font = `${style.fontWeight} ${style.fontSize}px ${style.fontFamily}`;
|
|
1879
|
+
context.fillStyle = style.color;
|
|
1880
|
+
context.textAlign = style.textAlign;
|
|
1881
|
+
const x = style.textAlign === "center" ? rect.x + rect.width / 2 : style.textAlign === "right" ? rect.x + rect.width - horizontalPadding : rect.x + horizontalPadding;
|
|
1882
|
+
context.fillText(
|
|
1883
|
+
ellipsizeGanttTaskText(context, content, contentWidth),
|
|
1884
|
+
x,
|
|
1885
|
+
rect.y + rect.height / 2
|
|
1886
|
+
);
|
|
1887
|
+
context.restore();
|
|
1888
|
+
}
|
|
1889
|
+
function drawResizeHandle(context, centerX, y, height, theme) {
|
|
1890
|
+
const handleHeight = Math.max(Math.min(height - 4, 18), 8);
|
|
1891
|
+
const handleY = y + (height - handleHeight) / 2;
|
|
1892
|
+
context.fillStyle = theme.backgroundColor;
|
|
1893
|
+
context.strokeStyle = theme.selectionColor;
|
|
1894
|
+
context.lineWidth = 1;
|
|
1895
|
+
context.beginPath();
|
|
1896
|
+
context.roundRect(
|
|
1897
|
+
centerX - HANDLE_WIDTH / 2,
|
|
1898
|
+
handleY,
|
|
1899
|
+
HANDLE_WIDTH,
|
|
1900
|
+
handleHeight,
|
|
1901
|
+
2
|
|
1902
|
+
);
|
|
1903
|
+
context.fill();
|
|
1904
|
+
context.stroke();
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// src/rendering/canvas-renderer.ts
|
|
1908
|
+
function renderGantt(context, layout, dependencies, selectedId, theme, renderViewport) {
|
|
1909
|
+
const viewport = renderViewport?.viewport ?? resolveGanttViewportLayout(
|
|
1910
|
+
layout,
|
|
1911
|
+
context.canvas.clientWidth || context.canvas.width,
|
|
1912
|
+
context.canvas.clientHeight || context.canvas.height
|
|
1913
|
+
);
|
|
1914
|
+
const scroll = renderViewport?.scroll ?? { left: 0, top: 0 };
|
|
1915
|
+
context.clearRect(0, 0, viewport.width, viewport.height);
|
|
1916
|
+
context.fillStyle = theme.backgroundColor;
|
|
1917
|
+
context.fillRect(0, 0, viewport.width, viewport.height);
|
|
1918
|
+
context.font = `${theme.fontSize}px ${theme.fontFamily}`;
|
|
1919
|
+
context.textBaseline = "middle";
|
|
1920
|
+
drawTimelineBody({
|
|
1921
|
+
context,
|
|
1922
|
+
layout,
|
|
1923
|
+
dependencies,
|
|
1924
|
+
selectedId,
|
|
1925
|
+
theme,
|
|
1926
|
+
viewport,
|
|
1927
|
+
scroll,
|
|
1928
|
+
taskResizable: renderViewport?.taskResizable !== false,
|
|
1929
|
+
weekendBackground: renderViewport?.weekendBackground !== false,
|
|
1930
|
+
rowDrag: renderViewport?.rowDrag,
|
|
1931
|
+
selectedDependencyIndex: renderViewport?.selectedDependencyIndex,
|
|
1932
|
+
dependencyDrag: renderViewport?.dependencyDrag,
|
|
1933
|
+
dependencyEditable: renderViewport?.dependencyEditable === true
|
|
1934
|
+
});
|
|
1935
|
+
drawTaskColumnBody(
|
|
1936
|
+
context,
|
|
1937
|
+
layout,
|
|
1938
|
+
theme,
|
|
1939
|
+
viewport,
|
|
1940
|
+
scroll.top,
|
|
1941
|
+
renderViewport?.rowDrag
|
|
1942
|
+
);
|
|
1943
|
+
drawRowDragInsertionIndicator(
|
|
1944
|
+
context,
|
|
1945
|
+
layout,
|
|
1946
|
+
viewport,
|
|
1947
|
+
scroll.top,
|
|
1948
|
+
renderViewport?.rowDrag
|
|
1949
|
+
);
|
|
1950
|
+
drawTimelineHeader(context, layout, theme, viewport, scroll.left);
|
|
1951
|
+
drawCornerHeader(
|
|
1952
|
+
context,
|
|
1953
|
+
layout,
|
|
1954
|
+
theme,
|
|
1955
|
+
renderViewport?.taskColumnTitle ?? "\u4EFB\u52A1"
|
|
1956
|
+
);
|
|
1957
|
+
drawGanttScrollbars({
|
|
1958
|
+
context,
|
|
1959
|
+
viewport,
|
|
1960
|
+
layout: renderViewport?.scrollbars ?? {},
|
|
1961
|
+
theme,
|
|
1962
|
+
headerHeight: layout.headerHeight
|
|
1963
|
+
});
|
|
1964
|
+
drawFixedBoundaries(context, layout, theme, viewport);
|
|
1965
|
+
}
|
|
1966
|
+
function drawTimelineBody(params) {
|
|
1967
|
+
const { context, layout, theme, viewport, scroll } = params;
|
|
1968
|
+
context.save();
|
|
1969
|
+
context.beginPath();
|
|
1970
|
+
context.rect(
|
|
1971
|
+
layout.taskColumnWidth,
|
|
1972
|
+
layout.headerHeight,
|
|
1973
|
+
viewport.timelineViewportWidth,
|
|
1974
|
+
viewport.bodyViewportHeight
|
|
1975
|
+
);
|
|
1976
|
+
context.clip();
|
|
1977
|
+
context.translate(-scroll.left, -scroll.top);
|
|
1978
|
+
drawTimelineBodyGrid(context, layout, theme, params.weekendBackground);
|
|
1979
|
+
drawTaskBars({
|
|
1980
|
+
context,
|
|
1981
|
+
layout,
|
|
1982
|
+
selectedId: params.selectedId,
|
|
1983
|
+
theme,
|
|
1984
|
+
resizable: params.taskResizable,
|
|
1985
|
+
rowOffsets: params.rowDrag?.offsets,
|
|
1986
|
+
draggedRowId: params.rowDrag?.phase === "dragging" ? params.rowDrag.taskId : void 0
|
|
1987
|
+
});
|
|
1988
|
+
drawDependencies(
|
|
1989
|
+
context,
|
|
1990
|
+
layout,
|
|
1991
|
+
params.dependencies,
|
|
1992
|
+
theme,
|
|
1993
|
+
params.rowDrag?.offsets,
|
|
1994
|
+
params.selectedDependencyIndex
|
|
1995
|
+
);
|
|
1996
|
+
if (params.dependencyEditable) {
|
|
1997
|
+
drawDependencyEditor({
|
|
1998
|
+
context,
|
|
1999
|
+
layout,
|
|
2000
|
+
selectedTaskId: params.selectedId,
|
|
2001
|
+
drag: params.dependencyDrag ?? null,
|
|
2002
|
+
theme
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
context.restore();
|
|
2006
|
+
}
|
|
2007
|
+
function drawTaskColumnBody(context, layout, theme, viewport, scrollTop, rowDrag) {
|
|
2008
|
+
context.save();
|
|
2009
|
+
context.beginPath();
|
|
2010
|
+
context.rect(
|
|
2011
|
+
0,
|
|
2012
|
+
layout.headerHeight,
|
|
2013
|
+
layout.taskColumnWidth,
|
|
2014
|
+
viewport.bodyViewportHeight
|
|
2015
|
+
);
|
|
2016
|
+
context.clip();
|
|
2017
|
+
context.fillStyle = theme.backgroundColor;
|
|
2018
|
+
context.fillRect(
|
|
2019
|
+
0,
|
|
2020
|
+
layout.headerHeight,
|
|
2021
|
+
layout.taskColumnWidth,
|
|
2022
|
+
viewport.bodyViewportHeight
|
|
2023
|
+
);
|
|
2024
|
+
context.translate(0, -scrollTop);
|
|
2025
|
+
drawTaskColumnGrid(context, layout, theme);
|
|
2026
|
+
layout.rows.forEach(({ task, y }) => {
|
|
2027
|
+
const visualY = y + (rowDrag?.offsets.get(task.id) ?? 0);
|
|
2028
|
+
const isDragging = rowDrag?.phase === "dragging" && rowDrag.taskId === task.id;
|
|
2029
|
+
if (isDragging) {
|
|
2030
|
+
context.save();
|
|
2031
|
+
context.globalAlpha = 0.94;
|
|
2032
|
+
context.shadowColor = "rgba(31, 35, 41, 0.16)";
|
|
2033
|
+
context.shadowBlur = 8;
|
|
2034
|
+
context.shadowOffsetY = 3;
|
|
2035
|
+
context.fillStyle = theme.backgroundColor;
|
|
2036
|
+
context.fillRect(0, visualY, layout.taskColumnWidth, layout.rowHeight);
|
|
2037
|
+
context.restore();
|
|
2038
|
+
}
|
|
2039
|
+
context.fillStyle = theme.textColor;
|
|
2040
|
+
context.fillText(task.title, 16, visualY + layout.rowHeight / 2);
|
|
2041
|
+
});
|
|
2042
|
+
context.restore();
|
|
2043
|
+
}
|
|
2044
|
+
function drawRowDragInsertionIndicator(context, layout, viewport, scrollTop, rowDrag) {
|
|
2045
|
+
if (!rowDrag || rowDrag.phase !== "dragging") return;
|
|
2046
|
+
const y = layout.headerHeight + rowDrag.targetIndex * layout.rowHeight - scrollTop;
|
|
2047
|
+
if (y < layout.headerHeight || y > viewport.contentBottom) return;
|
|
2048
|
+
context.save();
|
|
2049
|
+
context.strokeStyle = "#1677ff";
|
|
2050
|
+
context.fillStyle = "#1677ff";
|
|
2051
|
+
context.lineWidth = 2;
|
|
2052
|
+
context.beginPath();
|
|
2053
|
+
context.moveTo(0, Math.round(y) + 0.5);
|
|
2054
|
+
context.lineTo(viewport.contentRight, Math.round(y) + 0.5);
|
|
2055
|
+
context.stroke();
|
|
2056
|
+
context.beginPath();
|
|
2057
|
+
context.arc(5, Math.round(y) + 0.5, 4, 0, Math.PI * 2);
|
|
2058
|
+
context.fill();
|
|
2059
|
+
context.restore();
|
|
2060
|
+
}
|
|
2061
|
+
function drawTimelineHeader(context, layout, theme, viewport, scrollLeft) {
|
|
2062
|
+
context.save();
|
|
2063
|
+
context.beginPath();
|
|
2064
|
+
context.rect(
|
|
2065
|
+
layout.taskColumnWidth,
|
|
2066
|
+
0,
|
|
2067
|
+
viewport.timelineViewportWidth,
|
|
2068
|
+
layout.headerHeight
|
|
2069
|
+
);
|
|
2070
|
+
context.clip();
|
|
2071
|
+
context.fillStyle = theme.headerBackgroundColor;
|
|
2072
|
+
context.fillRect(
|
|
2073
|
+
layout.taskColumnWidth,
|
|
2074
|
+
0,
|
|
2075
|
+
viewport.timelineViewportWidth,
|
|
2076
|
+
layout.headerHeight
|
|
2077
|
+
);
|
|
2078
|
+
context.translate(-scrollLeft, 0);
|
|
2079
|
+
context.font = `600 ${theme.fontSize}px ${theme.fontFamily}`;
|
|
2080
|
+
const visibleLeft = layout.taskColumnWidth + scrollLeft;
|
|
2081
|
+
const visibleRight = visibleLeft + viewport.timelineViewportWidth;
|
|
2082
|
+
context.textAlign = "center";
|
|
2083
|
+
layout.headerLevels.forEach((level) => {
|
|
2084
|
+
buildTimelineHeaderCells(layout, level).forEach((cell) => {
|
|
2085
|
+
drawVerticalLine(
|
|
2086
|
+
context,
|
|
2087
|
+
cell.x,
|
|
2088
|
+
level.y,
|
|
2089
|
+
level.y + level.height,
|
|
2090
|
+
theme.borderColor
|
|
2091
|
+
);
|
|
2092
|
+
const cellRight = cell.x + cell.width;
|
|
2093
|
+
const clippedLeft = Math.max(cell.x, visibleLeft);
|
|
2094
|
+
const clippedRight = Math.min(cellRight, visibleRight);
|
|
2095
|
+
if (clippedRight <= clippedLeft) return;
|
|
2096
|
+
const availableWidth = clippedRight - clippedLeft - 16;
|
|
2097
|
+
if (availableWidth < 24) return;
|
|
2098
|
+
context.fillStyle = theme.mutedTextColor;
|
|
2099
|
+
context.fillText(
|
|
2100
|
+
cell.label,
|
|
2101
|
+
(clippedLeft + clippedRight) / 2,
|
|
2102
|
+
level.y + level.height / 2,
|
|
2103
|
+
availableWidth
|
|
2104
|
+
);
|
|
2105
|
+
});
|
|
2106
|
+
drawVerticalLine(
|
|
2107
|
+
context,
|
|
2108
|
+
layout.totalWidth,
|
|
2109
|
+
level.y,
|
|
2110
|
+
level.y + level.height,
|
|
2111
|
+
theme.borderColor
|
|
2112
|
+
);
|
|
2113
|
+
drawHorizontalLine(
|
|
2114
|
+
context,
|
|
2115
|
+
level.y + level.height,
|
|
2116
|
+
layout.taskColumnWidth,
|
|
2117
|
+
layout.totalWidth,
|
|
2118
|
+
theme.borderColor
|
|
2119
|
+
);
|
|
2120
|
+
});
|
|
2121
|
+
context.textAlign = "left";
|
|
2122
|
+
context.restore();
|
|
2123
|
+
}
|
|
2124
|
+
function drawCornerHeader(context, layout, theme, title) {
|
|
2125
|
+
context.fillStyle = theme.headerBackgroundColor;
|
|
2126
|
+
context.fillRect(0, 0, layout.taskColumnWidth, layout.headerHeight);
|
|
2127
|
+
context.font = `600 ${theme.fontSize}px ${theme.fontFamily}`;
|
|
2128
|
+
context.fillStyle = theme.textColor;
|
|
2129
|
+
context.fillText(title, 16, layout.headerHeight / 2);
|
|
2130
|
+
}
|
|
2131
|
+
function drawFixedBoundaries(context, layout, theme, viewport) {
|
|
2132
|
+
drawVerticalLine(
|
|
2133
|
+
context,
|
|
2134
|
+
layout.taskColumnWidth,
|
|
2135
|
+
0,
|
|
2136
|
+
viewport.contentBottom,
|
|
2137
|
+
theme.borderColor
|
|
2138
|
+
);
|
|
2139
|
+
drawHorizontalLine(
|
|
2140
|
+
context,
|
|
2141
|
+
layout.headerHeight,
|
|
2142
|
+
0,
|
|
2143
|
+
viewport.contentRight,
|
|
2144
|
+
theme.borderColor
|
|
2145
|
+
);
|
|
48
2146
|
}
|
|
49
2147
|
|
|
50
2148
|
// src/schema/options.ts
|
|
@@ -64,199 +2162,261 @@ var defaultGanttTheme = {
|
|
|
64
2162
|
fontSize: 14
|
|
65
2163
|
};
|
|
66
2164
|
|
|
67
|
-
// src/
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
context
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (task.progress) {
|
|
118
|
-
context.fillStyle = theme.taskProgressColor;
|
|
119
|
-
context.beginPath();
|
|
120
|
-
context.roundRect(x, barY, w * Math.min(100, Math.max(0, task.progress)) / 100, barH, 4);
|
|
121
|
-
context.fill();
|
|
122
|
-
}
|
|
123
|
-
if (selectedId === task.id) {
|
|
124
|
-
context.strokeStyle = theme.selectionColor;
|
|
125
|
-
context.lineWidth = 2;
|
|
126
|
-
context.strokeRect(x - 1, barY - 1, w + 2, barH + 2);
|
|
127
|
-
context.lineWidth = 1;
|
|
2165
|
+
// src/engine/render-service.ts
|
|
2166
|
+
function renderGanttFrame(params) {
|
|
2167
|
+
const { container, canvas, context, options, runtime } = params;
|
|
2168
|
+
const theme = { ...defaultGanttTheme, ...options.theme };
|
|
2169
|
+
container.style.boxSizing = "border-box";
|
|
2170
|
+
container.style.border = `1px solid ${theme.borderColor}`;
|
|
2171
|
+
container.style.borderRadius = "8px";
|
|
2172
|
+
const width = container.clientWidth || options.taskColumnWidth || 800;
|
|
2173
|
+
const height = container.clientHeight || 480;
|
|
2174
|
+
const ratio = window.devicePixelRatio || 1;
|
|
2175
|
+
const resolvedOptions = runtime.taskColumnWidth == null ? options : { ...options, taskColumnWidth: runtime.taskColumnWidth };
|
|
2176
|
+
let layout = buildGanttLayout(resolvedOptions);
|
|
2177
|
+
let viewport = resolveGanttViewportLayout(layout, width, height);
|
|
2178
|
+
layout = fitGanttLayoutToTimelineWidth(
|
|
2179
|
+
layout,
|
|
2180
|
+
viewport.timelineViewportWidth
|
|
2181
|
+
);
|
|
2182
|
+
viewport = resolveGanttViewportLayout(layout, width, height);
|
|
2183
|
+
const scroll = clampGanttScroll(runtime.scroll, viewport.bounds);
|
|
2184
|
+
const scrollbars = computeGanttScrollbarLayout({ layout, viewport, scroll });
|
|
2185
|
+
runtime.layout = layout;
|
|
2186
|
+
runtime.viewport = viewport;
|
|
2187
|
+
runtime.scroll = scroll;
|
|
2188
|
+
runtime.scrollbars = scrollbars;
|
|
2189
|
+
const pixelWidth = Math.max(Math.round(width * ratio), 1);
|
|
2190
|
+
const pixelHeight = Math.max(Math.round(height * ratio), 1);
|
|
2191
|
+
if (canvas.width !== pixelWidth) canvas.width = pixelWidth;
|
|
2192
|
+
if (canvas.height !== pixelHeight) canvas.height = pixelHeight;
|
|
2193
|
+
const cssWidth = `${width}px`;
|
|
2194
|
+
const cssHeight = `${height}px`;
|
|
2195
|
+
if (canvas.style.width !== cssWidth) canvas.style.width = cssWidth;
|
|
2196
|
+
if (canvas.style.height !== cssHeight) canvas.style.height = cssHeight;
|
|
2197
|
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
2198
|
+
renderGantt(
|
|
2199
|
+
context,
|
|
2200
|
+
layout,
|
|
2201
|
+
resolvedOptions.dependencies ?? [],
|
|
2202
|
+
runtime.selectedId,
|
|
2203
|
+
{ ...defaultGanttTheme, ...resolvedOptions.theme },
|
|
2204
|
+
{
|
|
2205
|
+
viewport,
|
|
2206
|
+
scroll,
|
|
2207
|
+
scrollbars,
|
|
2208
|
+
taskColumnTitle: resolvedOptions.taskColumnTitle,
|
|
2209
|
+
taskResizable: resolvedOptions.editable === true && resolvedOptions.taskResizable !== false,
|
|
2210
|
+
weekendBackground: resolvedOptions.weekendBackground !== false,
|
|
2211
|
+
rowDrag: runtime.rowDrag,
|
|
2212
|
+
selectedDependencyIndex: runtime.selectedDependencyIndex,
|
|
2213
|
+
dependencyDrag: runtime.dependencyDrag,
|
|
2214
|
+
dependencyEditable: resolvedOptions.editable === true
|
|
128
2215
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
context.closePath();
|
|
152
|
-
context.fill();
|
|
153
|
-
});
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// src/engine/runtime.ts
|
|
2220
|
+
function createGanttRuntime() {
|
|
2221
|
+
return {
|
|
2222
|
+
layout: null,
|
|
2223
|
+
viewport: null,
|
|
2224
|
+
scrollbars: {},
|
|
2225
|
+
scroll: { left: 0, top: 0 },
|
|
2226
|
+
selectedId: null,
|
|
2227
|
+
selectedDependencyIndex: null,
|
|
2228
|
+
dependencyDrag: null,
|
|
2229
|
+
taskDrag: null,
|
|
2230
|
+
scrollbarDrag: null,
|
|
2231
|
+
columnResize: null,
|
|
2232
|
+
taskColumnWidth: null,
|
|
2233
|
+
wheelScrollTarget: null,
|
|
2234
|
+
wheelScrollFrameId: null,
|
|
2235
|
+
wheelScrollLastTime: 0,
|
|
2236
|
+
rowDrag: null
|
|
2237
|
+
};
|
|
154
2238
|
}
|
|
155
2239
|
|
|
156
2240
|
// src/gantt.ts
|
|
157
2241
|
var Gantt = class {
|
|
158
2242
|
constructor(container, options) {
|
|
159
|
-
__publicField(this, "container");
|
|
2243
|
+
__publicField(this, "container", container);
|
|
160
2244
|
__publicField(this, "canvas");
|
|
161
2245
|
__publicField(this, "context");
|
|
2246
|
+
__publicField(this, "runtime", createGanttRuntime());
|
|
2247
|
+
__publicField(this, "eventController");
|
|
162
2248
|
__publicField(this, "options");
|
|
163
|
-
__publicField(this, "layout", null);
|
|
164
|
-
__publicField(this, "selectedId", null);
|
|
165
|
-
__publicField(this, "scrollLeft", 0);
|
|
166
|
-
__publicField(this, "scrollTop", 0);
|
|
167
2249
|
__publicField(this, "resizeObserver", null);
|
|
168
|
-
__publicField(this, "
|
|
169
|
-
__publicField(this, "handlePointerDown", (event) => {
|
|
170
|
-
if (!this.layout) return;
|
|
171
|
-
const point = this.point(event);
|
|
172
|
-
const task = hitTestTask(point.x, point.y, this.layout);
|
|
173
|
-
if (!task) return;
|
|
174
|
-
this.selectedId = task.id;
|
|
175
|
-
this.drag = { task, startX: point.x, originalStart: new Date(task.start).getTime(), originalEnd: new Date(task.end).getTime() };
|
|
176
|
-
this.canvas.setPointerCapture(event.pointerId);
|
|
177
|
-
this.options.onTaskClick?.(task);
|
|
178
|
-
this.render();
|
|
179
|
-
});
|
|
180
|
-
__publicField(this, "handlePointerMove", (event) => {
|
|
181
|
-
if (!this.drag || !this.layout) return;
|
|
182
|
-
const delta = Math.round((this.point(event).x - this.drag.startX) / this.layout.dayWidth) * 864e5;
|
|
183
|
-
this.drag.task.start = new Date(this.drag.originalStart + delta);
|
|
184
|
-
this.drag.task.end = new Date(this.drag.originalEnd + delta);
|
|
185
|
-
this.render();
|
|
186
|
-
});
|
|
187
|
-
__publicField(this, "handlePointerUp", (event) => {
|
|
188
|
-
if (!this.drag) return;
|
|
189
|
-
this.options.onTaskChange?.(this.drag.task);
|
|
190
|
-
this.drag = null;
|
|
191
|
-
this.canvas.releasePointerCapture(event.pointerId);
|
|
192
|
-
});
|
|
193
|
-
__publicField(this, "handleWheel", (event) => {
|
|
194
|
-
event.preventDefault();
|
|
195
|
-
this.scrollLeft = Math.max(0, this.scrollLeft + event.deltaX + (event.shiftKey ? event.deltaY : 0));
|
|
196
|
-
this.scrollTop = Math.max(0, this.scrollTop + (event.shiftKey ? 0 : event.deltaY));
|
|
197
|
-
this.render();
|
|
198
|
-
});
|
|
2250
|
+
__publicField(this, "renderFrameId", null);
|
|
199
2251
|
const context = document.createElement("canvas").getContext("2d");
|
|
200
|
-
if (!context)
|
|
201
|
-
|
|
2252
|
+
if (!context) {
|
|
2253
|
+
throw new Error(
|
|
2254
|
+
"Canvas 2D context is not supported in the current environment."
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
202
2257
|
this.canvas = context.canvas;
|
|
203
2258
|
this.context = context;
|
|
204
2259
|
this.options = options;
|
|
205
2260
|
this.canvas.style.display = "block";
|
|
206
2261
|
this.canvas.style.touchAction = "none";
|
|
2262
|
+
this.canvas.style.outline = "none";
|
|
2263
|
+
this.canvas.tabIndex = 0;
|
|
207
2264
|
this.container.style.overflow = "hidden";
|
|
2265
|
+
this.container.style.borderRadius = "8px";
|
|
2266
|
+
this.eventController = new GanttEventController({
|
|
2267
|
+
canvas: this.canvas,
|
|
2268
|
+
runtime: this.runtime,
|
|
2269
|
+
getOptions: () => this.options,
|
|
2270
|
+
render: () => this.render(),
|
|
2271
|
+
scheduleRender: () => this.scheduleRender(),
|
|
2272
|
+
scrollTo: (left, top) => this.scrollTo(left, top)
|
|
2273
|
+
});
|
|
208
2274
|
}
|
|
209
2275
|
/** 挂载并开始渲染。 */
|
|
210
2276
|
mount() {
|
|
211
2277
|
if (!this.canvas.parentElement) this.container.appendChild(this.canvas);
|
|
212
|
-
this.canvas.addEventListener(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
2278
|
+
this.canvas.addEventListener(
|
|
2279
|
+
"pointerdown",
|
|
2280
|
+
this.eventController.handlePointerDown
|
|
2281
|
+
);
|
|
2282
|
+
this.canvas.addEventListener(
|
|
2283
|
+
"pointermove",
|
|
2284
|
+
this.eventController.handlePointerMove
|
|
2285
|
+
);
|
|
2286
|
+
this.canvas.addEventListener(
|
|
2287
|
+
"pointerup",
|
|
2288
|
+
this.eventController.handlePointerUp
|
|
2289
|
+
);
|
|
2290
|
+
this.canvas.addEventListener(
|
|
2291
|
+
"pointercancel",
|
|
2292
|
+
this.eventController.handlePointerUp
|
|
2293
|
+
);
|
|
2294
|
+
this.canvas.addEventListener(
|
|
2295
|
+
"pointerleave",
|
|
2296
|
+
this.eventController.handlePointerLeave
|
|
2297
|
+
);
|
|
2298
|
+
this.canvas.addEventListener("wheel", this.eventController.handleWheel, {
|
|
2299
|
+
passive: false
|
|
2300
|
+
});
|
|
2301
|
+
this.canvas.addEventListener("keydown", this.eventController.handleKeyDown);
|
|
216
2302
|
this.resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => this.render());
|
|
217
2303
|
this.resizeObserver?.observe(this.container);
|
|
218
2304
|
this.render();
|
|
219
2305
|
}
|
|
220
2306
|
/** 更新配置并重绘。 */
|
|
221
2307
|
updateOptions(options) {
|
|
2308
|
+
this.eventController.stopWheelScroll();
|
|
2309
|
+
if (options.scale !== this.options.scale) {
|
|
2310
|
+
this.runtime.scroll.left = 0;
|
|
2311
|
+
}
|
|
2312
|
+
if (options.taskColumnWidth !== this.options.taskColumnWidth) {
|
|
2313
|
+
this.runtime.taskColumnWidth = null;
|
|
2314
|
+
}
|
|
222
2315
|
this.options = options;
|
|
223
|
-
this.
|
|
2316
|
+
if (this.runtime.selectedDependencyIndex != null && this.runtime.selectedDependencyIndex >= (options.dependencies?.length ?? 0)) {
|
|
2317
|
+
this.runtime.selectedDependencyIndex = null;
|
|
2318
|
+
}
|
|
224
2319
|
this.render();
|
|
225
2320
|
}
|
|
226
2321
|
/** 获取当前选中任务。 */
|
|
227
2322
|
getSelectedTask() {
|
|
228
|
-
return this.options.tasks
|
|
2323
|
+
return findGanttTaskById(this.options.tasks, this.runtime.selectedId);
|
|
2324
|
+
}
|
|
2325
|
+
/** 获取当前选中的依赖连线。 */
|
|
2326
|
+
getSelectedDependency() {
|
|
2327
|
+
return this.runtime.selectedDependencyIndex == null ? null : this.options.dependencies?.[this.runtime.selectedDependencyIndex] ?? null;
|
|
2328
|
+
}
|
|
2329
|
+
/** 滚动到指定位置,超出内容范围的坐标会被自动约束。 */
|
|
2330
|
+
scrollTo(left, top = this.runtime.scroll.top) {
|
|
2331
|
+
this.eventController.stopWheelScroll();
|
|
2332
|
+
const viewport = this.runtime.viewport;
|
|
2333
|
+
if (!viewport) return;
|
|
2334
|
+
this.runtime.scroll = clampGanttScroll({ left, top }, viewport.bounds);
|
|
2335
|
+
this.render();
|
|
229
2336
|
}
|
|
230
2337
|
/** 销毁实例。 */
|
|
231
2338
|
destroy() {
|
|
2339
|
+
this.eventController.destroy();
|
|
2340
|
+
if (this.renderFrameId != null) {
|
|
2341
|
+
cancelAnimationFrame(this.renderFrameId);
|
|
2342
|
+
this.renderFrameId = null;
|
|
2343
|
+
}
|
|
232
2344
|
this.resizeObserver?.disconnect();
|
|
233
2345
|
this.resizeObserver = null;
|
|
2346
|
+
this.canvas.removeEventListener(
|
|
2347
|
+
"pointerdown",
|
|
2348
|
+
this.eventController.handlePointerDown
|
|
2349
|
+
);
|
|
2350
|
+
this.canvas.removeEventListener(
|
|
2351
|
+
"pointermove",
|
|
2352
|
+
this.eventController.handlePointerMove
|
|
2353
|
+
);
|
|
2354
|
+
this.canvas.removeEventListener(
|
|
2355
|
+
"pointerup",
|
|
2356
|
+
this.eventController.handlePointerUp
|
|
2357
|
+
);
|
|
2358
|
+
this.canvas.removeEventListener(
|
|
2359
|
+
"pointercancel",
|
|
2360
|
+
this.eventController.handlePointerUp
|
|
2361
|
+
);
|
|
2362
|
+
this.canvas.removeEventListener(
|
|
2363
|
+
"pointerleave",
|
|
2364
|
+
this.eventController.handlePointerLeave
|
|
2365
|
+
);
|
|
2366
|
+
this.canvas.removeEventListener("wheel", this.eventController.handleWheel);
|
|
2367
|
+
this.canvas.removeEventListener(
|
|
2368
|
+
"keydown",
|
|
2369
|
+
this.eventController.handleKeyDown
|
|
2370
|
+
);
|
|
234
2371
|
this.canvas.remove();
|
|
235
2372
|
}
|
|
236
2373
|
render() {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
2374
|
+
if (this.renderFrameId != null) {
|
|
2375
|
+
cancelAnimationFrame(this.renderFrameId);
|
|
2376
|
+
this.renderFrameId = null;
|
|
2377
|
+
}
|
|
2378
|
+
renderGanttFrame({
|
|
2379
|
+
container: this.container,
|
|
2380
|
+
canvas: this.canvas,
|
|
2381
|
+
context: this.context,
|
|
2382
|
+
options: this.options,
|
|
2383
|
+
runtime: this.runtime
|
|
2384
|
+
});
|
|
247
2385
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
2386
|
+
scheduleRender() {
|
|
2387
|
+
if (this.renderFrameId != null) return;
|
|
2388
|
+
this.renderFrameId = requestAnimationFrame(() => {
|
|
2389
|
+
this.renderFrameId = null;
|
|
2390
|
+
this.render();
|
|
2391
|
+
});
|
|
251
2392
|
}
|
|
252
2393
|
};
|
|
253
2394
|
|
|
2395
|
+
// src/interaction/hit-test.ts
|
|
2396
|
+
function hitTestTask(x, y, layout) {
|
|
2397
|
+
return hitTestTaskBar({
|
|
2398
|
+
x,
|
|
2399
|
+
y,
|
|
2400
|
+
layout,
|
|
2401
|
+
selectedId: null,
|
|
2402
|
+
resizable: false
|
|
2403
|
+
})?.task ?? null;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
254
2406
|
exports.Gantt = Gantt;
|
|
255
2407
|
exports.buildGanttLayout = buildGanttLayout;
|
|
2408
|
+
exports.clampGanttScroll = clampGanttScroll;
|
|
2409
|
+
exports.computeGanttScrollbarLayout = computeGanttScrollbarLayout;
|
|
256
2410
|
exports.defaultGanttTheme = defaultGanttTheme;
|
|
2411
|
+
exports.hitTestGanttDependency = hitTestGanttDependency;
|
|
2412
|
+
exports.hitTestGanttScrollbar = hitTestGanttScrollbar;
|
|
257
2413
|
exports.hitTestTask = hitTestTask;
|
|
258
2414
|
exports.renderGantt = renderGantt;
|
|
259
2415
|
exports.resolveDayWidth = resolveDayWidth;
|
|
2416
|
+
exports.resolveDependencyLayoutRoute = resolveDependencyLayoutRoute;
|
|
2417
|
+
exports.resolveDependencyRoute = resolveDependencyRoute;
|
|
2418
|
+
exports.resolveGanttScrollBounds = resolveGanttScrollBounds;
|
|
2419
|
+
exports.resolveGanttViewportLayout = resolveGanttViewportLayout;
|
|
260
2420
|
exports.xForDate = xForDate;
|
|
261
2421
|
//# sourceMappingURL=index.cjs.map
|
|
262
2422
|
//# sourceMappingURL=index.cjs.map
|