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