@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/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 timestamp = value instanceof Date ? value.getTime() : new Date(value).getTime();
10
- if (!Number.isFinite(timestamp)) throw new Error(`Invalid date value: ${String(value)}`);
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
- function formatDate(timestamp) {
20
- return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit" }).format(new Date(timestamp));
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" ? 42 : scale === "week" ? 28 : 80;
233
+ return scale === "month" ? 12 : scale === "week" ? 28 : 80;
26
234
  }
27
- function buildGanttLayout(options, width) {
28
- const timestamps = options.tasks.flatMap((task) => [toTimestamp(task.start), toTimestamp(task.end)]);
29
- const start = startOfDay(toTimestamp(options.startDate ?? Math.min(...timestamps, Date.now())));
30
- const end = startOfDay(toTimestamp(options.endDate ?? Math.max(...timestamps, Date.now()) + dayMs));
31
- const dayWidth = resolveDayWidth(options.scale ?? "day");
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 rows = options.tasks.map((task, index) => ({ task, index, y: (options.headerHeight ?? 56) + index * (options.rowHeight ?? 36) }));
35
- return { start, end, dayWidth, rowHeight: options.rowHeight ?? 36, headerHeight: options.headerHeight ?? 56, taskColumnWidth, rows, totalWidth: taskColumnWidth + days * dayWidth, totalHeight: (options.headerHeight ?? 56) + options.tasks.length * (options.rowHeight ?? 36) };
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/interaction/hit-test.ts
42
- function hitTestTask(x, y, layout) {
43
- const row = layout.rows.find((item) => y >= item.y && y < item.y + layout.rowHeight);
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
- const start = xForDate(toTimestamp(row.task.start), layout);
46
- const end = xForDate(toTimestamp(row.task.end), layout);
47
- return x >= start && x <= end ? row.task : null;
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/rendering/canvas-renderer.ts
68
- function renderGantt(context, layout, dependencies, selectedId, theme) {
69
- context.clearRect(0, 0, context.canvas.width, context.canvas.height);
70
- context.fillStyle = theme.backgroundColor;
71
- context.fillRect(0, 0, context.canvas.width, context.canvas.height);
72
- context.font = `${theme.fontSize}px ${theme.fontFamily}`;
73
- context.textBaseline = "middle";
74
- context.fillStyle = theme.headerBackgroundColor;
75
- context.fillRect(0, 0, layout.totalWidth, layout.headerHeight);
76
- context.strokeStyle = theme.borderColor;
77
- context.beginPath();
78
- context.moveTo(layout.taskColumnWidth, 0);
79
- context.lineTo(layout.taskColumnWidth, layout.totalHeight);
80
- context.stroke();
81
- for (let t = layout.start, day = 0; t < layout.end; t += dayMs, day++) {
82
- const x = layout.taskColumnWidth + day * layout.dayWidth;
83
- const date = new Date(t);
84
- const weekend = date.getDay() === 0 || date.getDay() === 6;
85
- if (weekend) {
86
- context.fillStyle = theme.weekendBackgroundColor;
87
- context.fillRect(x, layout.headerHeight, layout.dayWidth, layout.totalHeight - layout.headerHeight);
88
- }
89
- context.strokeStyle = theme.gridColor;
90
- context.beginPath();
91
- context.moveTo(x, 0);
92
- context.lineTo(x, layout.totalHeight);
93
- context.stroke();
94
- context.fillStyle = theme.mutedTextColor;
95
- context.fillText(formatDate(t), x + 6, layout.headerHeight / 2);
96
- }
97
- context.fillStyle = theme.textColor;
98
- context.fillText("\u4EFB\u52A1", 16, layout.headerHeight / 2);
99
- layout.rows.forEach(({ task, y }) => {
100
- context.strokeStyle = theme.gridColor;
101
- context.beginPath();
102
- context.moveTo(0, y);
103
- context.lineTo(layout.totalWidth, y);
104
- context.stroke();
105
- context.fillStyle = theme.textColor;
106
- context.fillText(task.title, 16, y + layout.rowHeight / 2);
107
- const start = toTimestamp(task.start);
108
- const end = Math.max(start + dayMs, toTimestamp(task.end));
109
- const x = xForDate(start, layout);
110
- const w = Math.max(8, (end - start) / dayMs * layout.dayWidth - 4);
111
- const barY = y + 8;
112
- const barH = layout.rowHeight - 16;
113
- context.fillStyle = task.color ?? theme.taskColor;
114
- context.beginPath();
115
- context.roundRect(x, barY, w, barH, 4);
116
- context.fill();
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
- const rowMap = new Map(layout.rows.map((row) => [row.task.id, row]));
131
- context.strokeStyle = theme.milestoneColor;
132
- context.fillStyle = theme.milestoneColor;
133
- dependencies.forEach((dependency) => {
134
- const from = rowMap.get(dependency.from);
135
- const to = rowMap.get(dependency.to);
136
- if (!from || !to) return;
137
- const fromX = xForDate(toTimestamp(from.task.end), layout);
138
- const fromY = from.y + layout.rowHeight / 2;
139
- const toX = xForDate(toTimestamp(to.task.start), layout);
140
- const toY = to.y + layout.rowHeight / 2;
141
- context.beginPath();
142
- context.moveTo(fromX, fromY);
143
- context.lineTo((fromX + toX) / 2, fromY);
144
- context.lineTo((fromX + toX) / 2, toY);
145
- context.lineTo(toX, toY);
146
- context.stroke();
147
- context.beginPath();
148
- context.moveTo(toX, toY);
149
- context.lineTo(toX - 5, toY - 4);
150
- context.lineTo(toX - 5, toY + 4);
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, "drag", null);
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) throw new Error("Canvas 2D context is not supported in the current environment.");
201
- this.container = container;
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("pointerdown", this.handlePointerDown);
213
- this.canvas.addEventListener("pointermove", this.handlePointerMove);
214
- this.canvas.addEventListener("pointerup", this.handlePointerUp);
215
- this.canvas.addEventListener("wheel", this.handleWheel, { passive: false });
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.selectedId = null;
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.find((task) => task.id === this.selectedId) ?? null;
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
- const width = this.container.clientWidth || this.options.taskColumnWidth || 800;
238
- const height = this.container.clientHeight || 480;
239
- const ratio = window.devicePixelRatio || 1;
240
- this.layout = buildGanttLayout(this.options);
241
- this.canvas.width = width * ratio;
242
- this.canvas.height = height * ratio;
243
- this.canvas.style.width = `${width}px`;
244
- this.canvas.style.height = `${height}px`;
245
- this.context.setTransform(ratio, 0, 0, ratio, -this.scrollLeft * ratio, -this.scrollTop * ratio);
246
- renderGantt(this.context, this.layout, this.options.dependencies ?? [], this.selectedId, { ...defaultGanttTheme, ...this.options.theme });
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
- point(event) {
249
- const rect = this.canvas.getBoundingClientRect();
250
- return { x: event.clientX - rect.left + this.scrollLeft, y: event.clientY - rect.top + this.scrollTop };
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