gantt-renderer 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -30,6 +30,8 @@ const taskBase = {
30
30
  parent: z.number().int().positive().optional(),
31
31
  /** Optional CSS color value for the task bar. Overrides the default color assignment. */
32
32
  color: z.string().optional(),
33
+ /** When `true`, the task bar cannot be dragged or resized. */
34
+ readonly: z.boolean().optional(),
33
35
  /** Optional arbitrary metadata for consumer use. Preserved in the parsed output. */
34
36
  data: z.record(z.string(), z.unknown()).optional()
35
37
  };
@@ -86,6 +88,8 @@ const LinkSchema = z.object({
86
88
  * @default 'FS'
87
89
  */
88
90
  type: LinkTypeSchema.default("FS"),
91
+ /** When `true`, the link cannot be modified or deleted through the UI. */
92
+ readonly: z.boolean().optional(),
89
93
  /** Optional arbitrary metadata for consumer use. Preserved in the parsed output. */
90
94
  data: z.record(z.string(), z.unknown()).optional()
91
95
  }).refine((l) => l.source !== l.target, {
@@ -1748,7 +1752,7 @@ function buildRow(row, selectedId, expandedIds, cbs, columns, locale) {
1748
1752
  wrapper.addEventListener("keydown", (event) => {
1749
1753
  if (event.key === "Enter" || event.key === " ") {
1750
1754
  event.preventDefault();
1751
- cbs.onTaskSelect(row.id);
1755
+ cbs.onTaskClick(row.id);
1752
1756
  }
1753
1757
  });
1754
1758
  for (const column of visibleColumns(columns)) wrapper.append(buildCell(column, row, expandedIds, cbs, locale));
@@ -2056,6 +2060,7 @@ function toTask(node) {
2056
2060
  startDate: node.startDate,
2057
2061
  ...node.parent === void 0 ? {} : { parent: node.parent },
2058
2062
  ...node.color === void 0 ? {} : { color: node.color },
2063
+ ...node.readonly === void 0 ? {} : { readonly: node.readonly },
2059
2064
  ...node.data === void 0 ? {} : { data: node.data }
2060
2065
  };
2061
2066
  switch (node.kind) {
@@ -2098,7 +2103,7 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
2098
2103
  try {
2099
2104
  barEl.setPointerCapture(e.pointerId);
2100
2105
  } catch {}
2101
- cbs.onTaskSelect?.(task.id);
2106
+ cbs.onTaskClick?.(task.id);
2102
2107
  const startX = e.clientX;
2103
2108
  const originDate = parseDate(task.startDate);
2104
2109
  const mapper = getMapper();
@@ -2115,7 +2120,7 @@ function attachDrag(barEl, resizeHandleEl, task, getMapper, cbs) {
2115
2120
  window.removeEventListener("pointermove", onMove);
2116
2121
  window.removeEventListener("pointerup", onUp);
2117
2122
  barEl.style.cursor = "grab";
2118
- cbs._onTaskMoveFinal?.({
2123
+ if (lastHours !== 0) cbs._onTaskMoveFinal?.({
2119
2124
  id: task.id,
2120
2125
  startDate: addHours(originDate, lastHours)
2121
2126
  });
@@ -2186,7 +2191,7 @@ function attachProgressDrag(progressEl, barEl, task, _getMapper, cbs) {
2186
2191
  if (e.button !== 0) return;
2187
2192
  e.preventDefault();
2188
2193
  e.stopPropagation();
2189
- cbs.onTaskSelect?.(task.id);
2194
+ cbs.onTaskClick?.(task.id);
2190
2195
  try {
2191
2196
  progressEl.setPointerCapture(e.pointerId);
2192
2197
  } catch {}
@@ -2231,7 +2236,7 @@ function attachProgressDrag(progressEl, barEl, task, _getMapper, cbs) {
2231
2236
  */
2232
2237
  function attachMilestoneClick(diamondEl, taskId, cbs) {
2233
2238
  function onClick() {
2234
- cbs.onTaskSelect?.(taskId);
2239
+ cbs.onTaskClick?.(taskId);
2235
2240
  }
2236
2241
  function onDoubleClick(event) {
2237
2242
  if (event.detail === 2) {
@@ -2361,11 +2366,16 @@ function createRightPaneRefs() {
2361
2366
  scrollContainer.append(stripeContainer);
2362
2367
  scrollContainer.append(absoluteLayer);
2363
2368
  absoluteLayer.append(svgLayer);
2369
+ const tooltipEl = el("div");
2370
+ tooltipEl.className = "gantt-tooltip";
2371
+ tooltipEl.style.display = "none";
2372
+ scrollContainer.append(tooltipEl);
2364
2373
  return {
2365
2374
  scrollContainer,
2366
2375
  stripeContainer,
2367
2376
  absoluteLayer,
2368
2377
  svgLayer,
2378
+ tooltipEl,
2369
2379
  barRegistry: /* @__PURE__ */ new Map()
2370
2380
  };
2371
2381
  }
@@ -2410,8 +2420,9 @@ function renderSpecialDayBackgrounds(layer, beforeNode, state, contentHeight) {
2410
2420
  cur = next;
2411
2421
  }
2412
2422
  }
2413
- function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, cbs) {
2423
+ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, cbs, tooltipEl) {
2414
2424
  const selected = task.id === selectedId;
2425
+ const readonly = task.readonly === true;
2415
2426
  const color = BAR_COLOR[layout.kind] ?? BAR_COLOR["task"];
2416
2427
  const bar = el("div");
2417
2428
  bar.className = `gantt-bar${selected ? " gantt-bar--selected gantt-shape--selected" : ""}`;
@@ -2423,7 +2434,7 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
2423
2434
  height: `${layout.height}px`,
2424
2435
  ...color === void 0 ? {} : { background: color },
2425
2436
  borderRadius: layout.kind === "project" ? "3px" : "4px",
2426
- cursor: "grab",
2437
+ cursor: readonly ? "pointer" : "grab",
2427
2438
  userSelect: "none",
2428
2439
  overflow: "hidden",
2429
2440
  zIndex: selected ? "3" : "2",
@@ -2474,30 +2485,38 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
2474
2485
  bar.setAttribute("aria-label", ariaLabel(state.locale, "ariaTask", task.text));
2475
2486
  bar.setAttribute("aria-pressed", String(selected));
2476
2487
  bar.dataset["taskId"] = String(task.id);
2477
- bar.addEventListener("click", () => {
2478
- cbs.onTaskSelect?.(task.id);
2488
+ bar.addEventListener("click", (event) => {
2489
+ if (event.detail === 2) cbs.onTaskDoubleClick?.({
2490
+ id: task.id,
2491
+ task: toTask(task)
2492
+ });
2493
+ else cbs.onTaskClick?.(task.id);
2479
2494
  });
2480
2495
  bar.addEventListener("keydown", (event) => {
2481
2496
  if (event.key === "Enter" || event.key === " ") {
2482
2497
  event.preventDefault();
2483
- cbs.onTaskSelect?.(task.id);
2498
+ cbs.onTaskClick?.(task.id);
2484
2499
  }
2485
2500
  });
2486
- const handle = el("div");
2487
- handle.className = "gantt-resize-handle";
2488
- css(handle, {
2489
- position: "absolute",
2490
- right: "0",
2491
- top: "0",
2492
- width: "8px",
2493
- height: "100%",
2494
- cursor: "ew-resize",
2495
- zIndex: "1",
2496
- touchAction: "none"
2497
- });
2498
- bar.append(handle);
2499
- layer.insertBefore(bar, svgLayer);
2500
- const cleanupDrag = attachDrag(bar, handle, task, () => state.mapper, cbs);
2501
+ let handle;
2502
+ let cleanupDrag;
2503
+ if (!readonly) {
2504
+ handle = el("div");
2505
+ handle.className = "gantt-resize-handle";
2506
+ css(handle, {
2507
+ position: "absolute",
2508
+ right: "0",
2509
+ top: "0",
2510
+ width: "8px",
2511
+ height: "100%",
2512
+ cursor: "ew-resize",
2513
+ zIndex: "1",
2514
+ touchAction: "none"
2515
+ });
2516
+ bar.append(handle);
2517
+ layer.insertBefore(bar, svgLayer);
2518
+ cleanupDrag = attachDrag(bar, handle, task, () => state.mapper, cbs);
2519
+ } else layer.insertBefore(bar, svgLayer);
2501
2520
  let cleanupLinkHandles;
2502
2521
  if (state.linkCreationEnabled) {
2503
2522
  const barCenterY = layout.y + layout.height / 2;
@@ -2532,17 +2551,52 @@ function renderBar(layer, svgLayer, task, layout, selectedId, registry, state, c
2532
2551
  bar.removeEventListener("mouseleave", onBarLeave);
2533
2552
  };
2534
2553
  }
2554
+ const onTooltipEnter = () => {
2555
+ const content = cbs.onTooltipText?.({
2556
+ id: task.id,
2557
+ task: toTask(task)
2558
+ });
2559
+ if (content && content.length > 0) {
2560
+ tooltipEl.innerHTML = content;
2561
+ tooltipEl.style.display = "";
2562
+ } else tooltipEl.style.display = "none";
2563
+ };
2564
+ const onTooltipMove = (e) => {
2565
+ const offsetX = 12;
2566
+ const offsetY = -8;
2567
+ let left = e.clientX + offsetX;
2568
+ let top = e.clientY + offsetY;
2569
+ const maxLeft = window.innerWidth - tooltipEl.offsetWidth - 4;
2570
+ const maxTop = window.innerHeight - tooltipEl.offsetHeight - 4;
2571
+ left = Math.max(4, Math.min(left, maxLeft));
2572
+ top = Math.max(4, Math.min(top, maxTop));
2573
+ tooltipEl.style.left = `${left}px`;
2574
+ tooltipEl.style.top = `${top}px`;
2575
+ };
2576
+ const onTooltipLeave = () => {
2577
+ tooltipEl.style.display = "none";
2578
+ };
2579
+ bar.addEventListener("mouseenter", onTooltipEnter);
2580
+ bar.addEventListener("mousemove", onTooltipMove);
2581
+ bar.addEventListener("mouseleave", onTooltipLeave);
2582
+ const cleanupTooltip = () => {
2583
+ bar.removeEventListener("mouseenter", onTooltipEnter);
2584
+ bar.removeEventListener("mousemove", onTooltipMove);
2585
+ bar.removeEventListener("mouseleave", onTooltipLeave);
2586
+ };
2535
2587
  const entry = {
2536
2588
  bar,
2537
- resizeHandle: handle,
2538
- cleanupDrag
2589
+ resizeHandle: handle ?? el("div")
2539
2590
  };
2591
+ if (cleanupDrag !== void 0) entry.cleanupDrag = cleanupDrag;
2540
2592
  if (cleanupLinkHandles !== void 0) entry.cleanupLinkHandles = cleanupLinkHandles;
2541
2593
  if (cleanupProgressDrag !== void 0) entry.cleanupProgressDrag = cleanupProgressDrag;
2594
+ if (cleanupTooltip !== void 0) entry.cleanupTooltip = cleanupTooltip;
2542
2595
  registry.set(task.id, entry);
2543
2596
  }
2544
- function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cbs, state) {
2597
+ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cbs, state, tooltipEl) {
2545
2598
  const selected = task.id === selectedId;
2599
+ const readonly = task.readonly === true;
2546
2600
  const size = MILESTONE_HALF * 2;
2547
2601
  const diamond = el("div");
2548
2602
  diamond.className = `gantt-milestone${selected ? " gantt-shape--selected" : ""}`;
@@ -2554,7 +2608,7 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
2554
2608
  height: `${size}px`,
2555
2609
  background: "var(--gantt-milestone)",
2556
2610
  transform: "rotate(45deg)",
2557
- cursor: "pointer",
2611
+ cursor: readonly ? "default" : "pointer",
2558
2612
  zIndex: "4"
2559
2613
  });
2560
2614
  diamond.tabIndex = 0;
@@ -2565,7 +2619,7 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
2565
2619
  diamond.addEventListener("keydown", (event) => {
2566
2620
  if (event.key === "Enter" || event.key === " ") {
2567
2621
  event.preventDefault();
2568
- cbs.onTaskSelect?.(task.id);
2622
+ cbs.onTaskClick?.(task.id);
2569
2623
  }
2570
2624
  });
2571
2625
  const labelEl = el("span");
@@ -2585,7 +2639,15 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
2585
2639
  layer.insertBefore(diamond, svgLayer);
2586
2640
  bindMilestoneTask(diamond, task);
2587
2641
  const dummy = el("div");
2588
- const cleanupDrag = attachMilestoneClick(diamond, task.id, cbs);
2642
+ let cleanupDrag;
2643
+ if (!readonly) cleanupDrag = attachMilestoneClick(diamond, task.id, cbs);
2644
+ else diamond.addEventListener("click", (event) => {
2645
+ if (event.detail === 2) cbs.onTaskDoubleClick?.({
2646
+ id: task.id,
2647
+ task: toTask(task)
2648
+ });
2649
+ else cbs.onTaskClick?.(task.id);
2650
+ });
2589
2651
  let cleanupLinkHandles;
2590
2652
  if (state.linkCreationEnabled) {
2591
2653
  const diamondCenterY = layout.y + layout.height / 2;
@@ -2611,12 +2673,46 @@ function renderMilestone(layer, svgLayer, task, layout, selectedId, registry, cb
2611
2673
  diamond.removeEventListener("mouseleave", onDiamondLeave);
2612
2674
  };
2613
2675
  }
2676
+ const onTooltipEnter = () => {
2677
+ const content = cbs.onTooltipText?.({
2678
+ id: task.id,
2679
+ task: toTask(task)
2680
+ });
2681
+ if (content && content.length > 0) {
2682
+ tooltipEl.innerHTML = content;
2683
+ tooltipEl.style.display = "";
2684
+ } else tooltipEl.style.display = "none";
2685
+ };
2686
+ const onTooltipMove = (e) => {
2687
+ const offsetX = 12;
2688
+ const offsetY = -8;
2689
+ let left = e.clientX + offsetX;
2690
+ let top = e.clientY + offsetY;
2691
+ const maxLeft = window.innerWidth - tooltipEl.offsetWidth - 4;
2692
+ const maxTop = window.innerHeight - tooltipEl.offsetHeight - 4;
2693
+ left = Math.max(4, Math.min(left, maxLeft));
2694
+ top = Math.max(4, Math.min(top, maxTop));
2695
+ tooltipEl.style.left = `${left}px`;
2696
+ tooltipEl.style.top = `${top}px`;
2697
+ };
2698
+ const onTooltipLeave = () => {
2699
+ tooltipEl.style.display = "none";
2700
+ };
2701
+ diamond.addEventListener("mouseenter", onTooltipEnter);
2702
+ diamond.addEventListener("mousemove", onTooltipMove);
2703
+ diamond.addEventListener("mouseleave", onTooltipLeave);
2704
+ const cleanupTooltip = () => {
2705
+ diamond.removeEventListener("mouseenter", onTooltipEnter);
2706
+ diamond.removeEventListener("mousemove", onTooltipMove);
2707
+ diamond.removeEventListener("mouseleave", onTooltipLeave);
2708
+ };
2614
2709
  const entry = {
2615
2710
  bar: diamond,
2616
- resizeHandle: dummy,
2617
- cleanupDrag
2711
+ resizeHandle: dummy
2618
2712
  };
2713
+ if (cleanupDrag !== void 0) entry.cleanupDrag = cleanupDrag;
2619
2714
  if (cleanupLinkHandles !== void 0) entry.cleanupLinkHandles = cleanupLinkHandles;
2715
+ if (cleanupTooltip !== void 0) entry.cleanupTooltip = cleanupTooltip;
2620
2716
  registry.set(task.id, entry);
2621
2717
  }
2622
2718
  /**
@@ -2665,7 +2761,7 @@ function renderRightPane(refs, state, cbs) {
2665
2761
  for (const node of toRemove) absoluteLayer.removeChild(node);
2666
2762
  hideGhostLine(svgLayer);
2667
2763
  for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag } of barRegistry.values()) {
2668
- cleanupDrag();
2764
+ cleanupDrag?.();
2669
2765
  cleanupLinkHandles?.();
2670
2766
  cleanupProgressDrag?.();
2671
2767
  }
@@ -2708,8 +2804,8 @@ function renderRightPane(refs, state, cbs) {
2708
2804
  for (const task of visibleRows) {
2709
2805
  const layout = layouts.get(task.id);
2710
2806
  if (layout === void 0) continue;
2711
- if (layout.kind === "milestone") renderMilestone(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, cbs, state);
2712
- else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs);
2807
+ if (layout.kind === "milestone") renderMilestone(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, cbs, state, refs.tooltipEl);
2808
+ else renderBar(absoluteLayer, svgLayer, task, layout, selectedId, barRegistry, state, cbs, refs.tooltipEl);
2713
2809
  }
2714
2810
  updateDependencyLayer(svgLayer, links.filter((link) => visibleTaskIds.has(link.sourceTaskId) && visibleTaskIds.has(link.targetTaskId)), totalWidth, contentHeight, selectedId, highlightLinkedDependenciesOnSelect, cbs);
2715
2811
  }
@@ -2869,15 +2965,16 @@ var GanttChart = class {
2869
2965
  /**
2870
2966
  * Constructs a new chart, builds the DOM, and wires internal event handling.
2871
2967
  * Data must be loaded via {@link update} before the chart renders.
2968
+ * Callbacks must be set via {@link setCallbacks} before user interactions are handled.
2872
2969
  *
2873
2970
  * @param container - The host `HTMLElement` the chart will be appended to.
2874
- * @param opts - Configuration and callback options.
2971
+ * @param opts - Configuration options.
2875
2972
  */
2876
- constructor(container, opts = {}, cbs = {}) {
2973
+ constructor(container, opts = {}) {
2877
2974
  this.#container = container;
2878
2975
  this.#scale = opts.scale ?? "day";
2879
2976
  this.#opts = opts;
2880
- this.#callbacks = cbs;
2977
+ this.#callbacks = {};
2881
2978
  this.#taskIndex = /* @__PURE__ */ new Map();
2882
2979
  this.#locale = resolveChartLocale(opts.locale);
2883
2980
  this.#columns = opts.gridColumns ?? gridColumnDefaults(this.#locale);
@@ -2887,21 +2984,39 @@ var GanttChart = class {
2887
2984
  this.#weekendDays = normalizeWeekendDays(opts.weekendDays ?? this.#locale.weekendDays);
2888
2985
  this.#specialDaysByDate = buildSpecialDayIndex(opts.specialDays ?? []);
2889
2986
  this.#expandedIds = /* @__PURE__ */ new Set();
2890
- this.#cbs = {
2891
- onTaskSelect: (id) => {
2987
+ this.#cbs = this.#buildCallbackAdapter();
2988
+ this.#buildDom();
2989
+ this.#wireEvents();
2990
+ container.append(this.#root);
2991
+ this.#applyTheme();
2992
+ this.#applyResponsivePaneStyles();
2993
+ this.#setupResizeObserver();
2994
+ }
2995
+ #buildCallbackAdapter() {
2996
+ return {
2997
+ onTaskClick: (id) => {
2892
2998
  if (this.#selectedId === id) return;
2893
2999
  this.#selectedId = id;
2894
3000
  if (this.#selectedId !== null) {
2895
3001
  const task = this.#findTask(this.#selectedId);
2896
- if (task !== void 0) this.#callbacks.onTaskSelect?.({ task });
3002
+ if (task !== void 0) this.#callbacks.onTaskClick?.({
3003
+ task,
3004
+ instance: this
3005
+ });
2897
3006
  }
2898
3007
  this.#scheduleRender();
2899
3008
  },
2900
3009
  onTaskDoubleClick: (payload) => {
2901
- this.#callbacks.onTaskDoubleClick?.({ task: payload.task });
3010
+ this.#callbacks.onTaskDoubleClick?.({
3011
+ task: payload.task,
3012
+ instance: this
3013
+ });
2902
3014
  },
2903
3015
  onTaskEditIntent: (payload) => {
2904
- this.#callbacks.onTaskDoubleClick?.({ task: payload.task });
3016
+ this.#callbacks.onTaskDoubleClick?.({
3017
+ task: payload.task,
3018
+ instance: this
3019
+ });
2905
3020
  },
2906
3021
  onTaskMove: (payload) => {
2907
3022
  if (!this.#dragOriginals.has(payload.id)) {
@@ -2912,13 +3027,20 @@ var GanttChart = class {
2912
3027
  this.#patchTask(payload.id, { startDate: iso });
2913
3028
  this.#scheduleRender();
2914
3029
  },
2915
- _onTaskMoveFinal: (payload) => {
3030
+ _onTaskMoveFinal: async (payload) => {
2916
3031
  const task = this.#findTask(payload.id);
2917
3032
  if (task !== void 0) {
2918
- if (this.#callbacks.onTaskMove?.({
3033
+ const result = this.#callbacks.onTaskMove?.({
2919
3034
  task,
2920
- newStartDate: payload.startDate
2921
- }) === false) {
3035
+ newStartDate: payload.startDate,
3036
+ instance: this
3037
+ });
3038
+ if (result instanceof Promise) {
3039
+ if (!await result) {
3040
+ const original = this.#dragOriginals.get(payload.id);
3041
+ if (original !== void 0) this.#patchTask(payload.id, { startDate: original.startDate });
3042
+ }
3043
+ } else if (!result) {
2922
3044
  const original = this.#dragOriginals.get(payload.id);
2923
3045
  if (original !== void 0) this.#patchTask(payload.id, { startDate: original.startDate });
2924
3046
  }
@@ -2935,13 +3057,20 @@ var GanttChart = class {
2935
3057
  this.#patchTask(payload.id, { durationHours: payload.durationHours });
2936
3058
  this.#scheduleRender();
2937
3059
  },
2938
- _onTaskResizeFinal: (payload) => {
3060
+ _onTaskResizeFinal: async (payload) => {
2939
3061
  const task = this.#findTask(payload.id);
2940
3062
  if (task !== void 0) {
2941
- if (this.#callbacks.onTaskResize?.({
3063
+ const result = this.#callbacks.onTaskResize?.({
2942
3064
  task,
2943
- newDurationHours: payload.durationHours
2944
- }) === false) {
3065
+ newDurationHours: payload.durationHours,
3066
+ instance: this
3067
+ });
3068
+ if (result instanceof Promise) {
3069
+ if (!await result) {
3070
+ const original = this.#dragOriginals.get(payload.id);
3071
+ if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { durationHours: original.durationHours });
3072
+ }
3073
+ } else if (!result) {
2945
3074
  const original = this.#dragOriginals.get(payload.id);
2946
3075
  if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { durationHours: original.durationHours });
2947
3076
  }
@@ -2958,13 +3087,20 @@ var GanttChart = class {
2958
3087
  this.#patchTask(payload.id, { percentComplete: payload.percentComplete });
2959
3088
  this.#scheduleRender();
2960
3089
  },
2961
- _onTaskProgressDragFinal: (payload) => {
3090
+ _onTaskProgressDragFinal: async (payload) => {
2962
3091
  const task = this.#findTask(payload.id);
2963
3092
  if (task !== void 0) {
2964
- if (this.#callbacks.onProgressChange?.({
3093
+ const result = this.#callbacks.onProgressChange?.({
2965
3094
  task,
2966
- newPercentComplete: payload.percentComplete
2967
- }) === false) {
3095
+ newPercentComplete: payload.percentComplete,
3096
+ instance: this
3097
+ });
3098
+ if (result instanceof Promise) {
3099
+ if (!await result) {
3100
+ const original = this.#dragOriginals.get(payload.id);
3101
+ if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { percentComplete: original.percentComplete });
3102
+ }
3103
+ } else if (!result) {
2968
3104
  const original = this.#dragOriginals.get(payload.id);
2969
3105
  if (original !== void 0 && original.kind !== "milestone") this.#patchTask(payload.id, { percentComplete: original.percentComplete });
2970
3106
  }
@@ -2975,13 +3111,22 @@ var GanttChart = class {
2975
3111
  },
2976
3112
  onTaskAdd: (parentId) => {
2977
3113
  const parentTask = this.#findTask(parentId);
2978
- if (parentTask !== void 0) this.#callbacks.onTaskAdd?.({ parentTask });
3114
+ if (parentTask !== void 0) this.#callbacks.onTaskAdd?.({
3115
+ parentTask,
3116
+ instance: this
3117
+ });
2979
3118
  },
2980
3119
  onLeftPaneWidthChange: (width) => {
2981
- this.#callbacks.onLeftPaneWidthChange?.(width);
3120
+ this.#callbacks.onLeftPaneWidthChange?.({
3121
+ width,
3122
+ instance: this
3123
+ });
2982
3124
  },
2983
3125
  onGridColumnsChange: (updatedColumns) => {
2984
- this.#callbacks.onGridColumnsChange?.(updatedColumns);
3126
+ this.#callbacks.onGridColumnsChange?.({
3127
+ columns: updatedColumns,
3128
+ instance: this
3129
+ });
2985
3130
  },
2986
3131
  onLinkCreate: (payload) => {
2987
3132
  const sourceTask = this.#findTask(payload.sourceTaskId);
@@ -2989,22 +3134,39 @@ var GanttChart = class {
2989
3134
  if (sourceTask !== void 0 && targetTask !== void 0) this.#callbacks.onLinkCreate?.({
2990
3135
  type: "FS",
2991
3136
  sourceTask,
2992
- targetTask
3137
+ targetTask,
3138
+ instance: this
2993
3139
  });
2994
3140
  },
2995
3141
  onLinkClick: (payload) => {
2996
- this.#callbacks.onLinkClick?.({ link: payload });
3142
+ this.#callbacks.onLinkClick?.({
3143
+ link: payload,
3144
+ instance: this
3145
+ });
2997
3146
  },
2998
3147
  onLinkDblClick: (payload) => {
2999
- this.#callbacks.onLinkDblClick?.({ link: payload });
3000
- }
3148
+ this.#callbacks.onLinkDblClick?.({
3149
+ link: payload,
3150
+ instance: this
3151
+ });
3152
+ },
3153
+ onTooltipText: (payload) => this.#callbacks.onTooltipText?.({
3154
+ task: payload.task,
3155
+ instance: this
3156
+ }) ?? null
3001
3157
  };
3002
- this.#buildDom();
3003
- this.#wireEvents();
3004
- container.append(this.#root);
3005
- this.#applyTheme();
3006
- this.#applyResponsivePaneStyles();
3007
- this.#setupResizeObserver();
3158
+ }
3159
+ /**
3160
+ * Sets or replaces the chart's user-facing callbacks.
3161
+ * Does not trigger a re-render.
3162
+ *
3163
+ * @param cbs - The {@link GanttCallbacks} to register.
3164
+ * @throws {GanttError} When the instance has been destroyed.
3165
+ */
3166
+ setCallbacks(cbs) {
3167
+ this.#assertAlive();
3168
+ this.#callbacks = cbs;
3169
+ this.#cbs = this.#buildCallbackAdapter();
3008
3170
  }
3009
3171
  /**
3010
3172
  * Replaces the full dataset and re-renders.
@@ -3085,14 +3247,18 @@ var GanttChart = class {
3085
3247
  * Programmatically selects or deselects a task.
3086
3248
  *
3087
3249
  * @param id - The task ID to select, or `null` to clear the selection.
3250
+ * @param fireCallback - Whether to fire the `onTaskClick` callback. Default `true`.
3088
3251
  * @throws {GanttError} When the instance has been destroyed.
3089
3252
  */
3090
- select(id) {
3253
+ select(id, fireCallback = true) {
3091
3254
  this.#assertAlive();
3092
3255
  if (id === null) this.#selectedId = null;
3093
3256
  else {
3094
3257
  const task = this.#input?.tasks.find((t) => t.id === id);
3095
- if (task !== void 0) this.#callbacks.onTaskSelect?.({ task });
3258
+ if (task !== void 0 && fireCallback) this.#callbacks.onTaskClick?.({
3259
+ task,
3260
+ instance: this
3261
+ });
3096
3262
  this.#selectedId = id;
3097
3263
  }
3098
3264
  if (this.#rafPending && this.#rafId !== null) {
@@ -3145,10 +3311,11 @@ var GanttChart = class {
3145
3311
  else window.removeEventListener("resize", this.#applyResponsivePaneStyles);
3146
3312
  if (this.#rafId !== null) cancelAnimationFrame(this.#rafId);
3147
3313
  this.#columnResizeCleanup();
3148
- for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag } of this.#rightPaneRefs.barRegistry.values()) {
3149
- cleanupDrag();
3314
+ for (const { cleanupDrag, cleanupLinkHandles, cleanupProgressDrag, cleanupTooltip } of this.#rightPaneRefs.barRegistry.values()) {
3315
+ cleanupDrag?.();
3150
3316
  cleanupLinkHandles?.();
3151
3317
  cleanupProgressDrag?.();
3318
+ cleanupTooltip?.();
3152
3319
  }
3153
3320
  clearChildren(this.#container);
3154
3321
  }
@@ -3181,7 +3348,7 @@ var GanttChart = class {
3181
3348
  id: payload.id,
3182
3349
  atMs: now
3183
3350
  };
3184
- this.#cbs.onTaskSelect?.(payload.id);
3351
+ this.#cbs.onTaskClick?.(payload.id);
3185
3352
  };
3186
3353
  #onScroll = () => {
3187
3354
  ({scrollTop: this.#scrollTop} = this.#scrollEl);
@@ -3265,7 +3432,7 @@ var GanttChart = class {
3265
3432
  else this.#expandedIds.add(id);
3266
3433
  this.#scheduleRender();
3267
3434
  },
3268
- onTaskSelect: (id) => this.#cbs.onTaskSelect?.(id),
3435
+ onTaskClick: (id) => this.#cbs.onTaskClick?.(id),
3269
3436
  onRowClick: (payload) => {
3270
3437
  this.#handleGridClick(payload);
3271
3438
  },