@ganttloom/gantt-core 0.2.1 → 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/README.md +96 -5
- package/dist/index.cjs +312 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.js +312 -32
- package/dist/index.js.map +1 -1
- package/dist/styles.css +51 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -614,6 +614,29 @@ function computeProgressRollup(node) {
|
|
|
614
614
|
}
|
|
615
615
|
node.computedProgress = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
616
616
|
}
|
|
617
|
+
function sortValue(node, col) {
|
|
618
|
+
if (!col) return null;
|
|
619
|
+
if (col.accessor) return col.accessor(node.task) ?? null;
|
|
620
|
+
return col.id === "name" ? node.task.name : null;
|
|
621
|
+
}
|
|
622
|
+
function compareValues(a, b) {
|
|
623
|
+
if (a === null && b === null) return 0;
|
|
624
|
+
if (a === null) return 1;
|
|
625
|
+
if (b === null) return -1;
|
|
626
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
627
|
+
return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
|
|
628
|
+
}
|
|
629
|
+
function sortTree(roots, columns, sort) {
|
|
630
|
+
if (!sort) return;
|
|
631
|
+
const col = columns.find((c) => c.id === sort.columnId);
|
|
632
|
+
const dir = sort.direction === "desc" ? -1 : 1;
|
|
633
|
+
const compare = (a, b) => dir * compareValues(sortValue(a, col), sortValue(b, col));
|
|
634
|
+
const sortLevel = (nodes) => {
|
|
635
|
+
nodes.sort(compare);
|
|
636
|
+
for (const node of nodes) sortLevel(node.children);
|
|
637
|
+
};
|
|
638
|
+
sortLevel(roots);
|
|
639
|
+
}
|
|
617
640
|
function flatten(roots) {
|
|
618
641
|
const out = [];
|
|
619
642
|
const stack = [];
|
|
@@ -700,6 +723,14 @@ function linkPath(from, to) {
|
|
|
700
723
|
const c2y = to.y;
|
|
701
724
|
return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
|
|
702
725
|
}
|
|
726
|
+
var MIN_VISIBLE_UNITS = {
|
|
727
|
+
hour: 8,
|
|
728
|
+
day: 7,
|
|
729
|
+
week: 6,
|
|
730
|
+
month: 6,
|
|
731
|
+
quarter: 4,
|
|
732
|
+
year: 5
|
|
733
|
+
};
|
|
703
734
|
function computeLayout(input) {
|
|
704
735
|
const {
|
|
705
736
|
tasks,
|
|
@@ -715,7 +746,8 @@ function computeLayout(input) {
|
|
|
715
746
|
markers = [],
|
|
716
747
|
autoRollupProgress = false,
|
|
717
748
|
selectedTaskIds,
|
|
718
|
-
pagination
|
|
749
|
+
pagination,
|
|
750
|
+
sort = null
|
|
719
751
|
} = input;
|
|
720
752
|
validateInputs(tasks, dependencies);
|
|
721
753
|
const { roots, nodeById } = buildTree(tasks);
|
|
@@ -723,6 +755,7 @@ function computeLayout(input) {
|
|
|
723
755
|
if (autoRollupProgress) {
|
|
724
756
|
for (const root of roots) computeProgressRollup(root);
|
|
725
757
|
}
|
|
758
|
+
sortTree(roots, columns, sort);
|
|
726
759
|
let visibleRoots = roots;
|
|
727
760
|
if (pagination && pagination.pageSize > 0) {
|
|
728
761
|
const start = Math.max(0, (pagination.page - 1) * pagination.pageSize);
|
|
@@ -742,8 +775,15 @@ function computeLayout(input) {
|
|
|
742
775
|
const now = Date.now();
|
|
743
776
|
const rawRangeStart = rangeMin === null ? new Date(now) : new Date(rangeMin);
|
|
744
777
|
const rawRangeEnd = rangeMax === null ? new Date(now + 1) : new Date(rangeMax);
|
|
745
|
-
|
|
746
|
-
|
|
778
|
+
let paddedStart = addUnit(rawRangeStart, viewMode, -1);
|
|
779
|
+
let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
|
|
780
|
+
const minUnits = MIN_VISIBLE_UNITS[viewMode];
|
|
781
|
+
const currentUnits = (paddedEnd.getTime() - paddedStart.getTime()) / approxUnitMs(viewMode);
|
|
782
|
+
if (currentUnits < minUnits) {
|
|
783
|
+
const deficit = minUnits - currentUnits;
|
|
784
|
+
paddedStart = addUnit(paddedStart, viewMode, -Math.ceil(deficit / 2));
|
|
785
|
+
paddedEnd = addUnit(paddedEnd, viewMode, Math.floor(deficit / 2));
|
|
786
|
+
}
|
|
747
787
|
const rows = [];
|
|
748
788
|
const bars = [];
|
|
749
789
|
const barByTaskId = /* @__PURE__ */ new Map();
|
|
@@ -894,7 +934,8 @@ function computeLayout(input) {
|
|
|
894
934
|
columns,
|
|
895
935
|
theme,
|
|
896
936
|
markers: renderMarkers,
|
|
897
|
-
rangeStart: paddedStart
|
|
937
|
+
rangeStart: paddedStart,
|
|
938
|
+
sort
|
|
898
939
|
};
|
|
899
940
|
}
|
|
900
941
|
|
|
@@ -1091,6 +1132,7 @@ var GanttRenderer = class {
|
|
|
1091
1132
|
this.lastModel = null;
|
|
1092
1133
|
this.lastTasks = [];
|
|
1093
1134
|
this.lastColumns = [];
|
|
1135
|
+
this.lastExplicitThemeKeys = /* @__PURE__ */ new Set();
|
|
1094
1136
|
this.onGridDoubleClick = (evt) => {
|
|
1095
1137
|
if (!this.options.onRenameCommit) return;
|
|
1096
1138
|
const target = evt.target;
|
|
@@ -1128,8 +1170,13 @@ var GanttRenderer = class {
|
|
|
1128
1170
|
this.root = el("div", "gantt-root");
|
|
1129
1171
|
this.gridPanel = el("div", "gantt-grid-panel");
|
|
1130
1172
|
this.timelineScroll = el("div", "gantt-timeline-scroll");
|
|
1173
|
+
this.headerContainer = el("div", "gantt-timeline-header");
|
|
1174
|
+
this.headerSvg = svgEl("svg");
|
|
1175
|
+
this.headerSvg.classList.add("gantt-header-svg");
|
|
1176
|
+
this.headerContainer.appendChild(this.headerSvg);
|
|
1131
1177
|
this.svg = svgEl("svg");
|
|
1132
1178
|
this.svg.classList.add("gantt-svg");
|
|
1179
|
+
this.timelineScroll.appendChild(this.headerContainer);
|
|
1133
1180
|
this.timelineScroll.appendChild(this.svg);
|
|
1134
1181
|
this.root.appendChild(this.gridPanel);
|
|
1135
1182
|
this.root.appendChild(this.timelineScroll);
|
|
@@ -1145,9 +1192,14 @@ var GanttRenderer = class {
|
|
|
1145
1192
|
this.lastTasks = tasks;
|
|
1146
1193
|
this.lastColumns = columns;
|
|
1147
1194
|
const cssVars = explicitThemeToCssVars(explicitTheme);
|
|
1148
|
-
|
|
1195
|
+
const newKeys = new Set(Object.keys(cssVars));
|
|
1196
|
+
for (const key of this.lastExplicitThemeKeys) {
|
|
1197
|
+
if (!newKeys.has(key)) this.root.style.removeProperty(key);
|
|
1198
|
+
}
|
|
1199
|
+
for (const key of newKeys) {
|
|
1149
1200
|
this.root.style.setProperty(key, cssVars[key]);
|
|
1150
1201
|
}
|
|
1202
|
+
this.lastExplicitThemeKeys = newKeys;
|
|
1151
1203
|
this.renderGridPanel(model, tasks, columns);
|
|
1152
1204
|
this.renderTimeline(model, tasks);
|
|
1153
1205
|
}
|
|
@@ -1175,7 +1227,7 @@ var GanttRenderer = class {
|
|
|
1175
1227
|
const header = el("div", "gantt-grid-header-row");
|
|
1176
1228
|
header.style.height = `${model.headerHeight}px`;
|
|
1177
1229
|
for (const col of columns) {
|
|
1178
|
-
header.appendChild(this.renderHeaderCell(col, columns));
|
|
1230
|
+
header.appendChild(this.renderHeaderCell(col, columns, model.sort));
|
|
1179
1231
|
}
|
|
1180
1232
|
this.gridPanel.appendChild(header);
|
|
1181
1233
|
const body = el("div", "gantt-grid-body");
|
|
@@ -1188,11 +1240,23 @@ var GanttRenderer = class {
|
|
|
1188
1240
|
});
|
|
1189
1241
|
this.gridPanel.appendChild(body);
|
|
1190
1242
|
}
|
|
1191
|
-
renderHeaderCell(col, columns) {
|
|
1243
|
+
renderHeaderCell(col, columns, sort) {
|
|
1192
1244
|
const cell = el("div", "gantt-grid-header-cell");
|
|
1193
1245
|
cell.style.position = "relative";
|
|
1194
1246
|
if (col.width) cell.style.width = `${col.width}px`;
|
|
1195
1247
|
cell.textContent = col.title;
|
|
1248
|
+
if (col.sortable) {
|
|
1249
|
+
cell.classList.add("gantt-grid-header-cell-sortable");
|
|
1250
|
+
const active = sort?.columnId === col.id;
|
|
1251
|
+
const indicator = el("span", "gantt-sort-indicator");
|
|
1252
|
+
indicator.textContent = active ? sort.direction === "asc" ? " \u25B2" : " \u25BC" : "";
|
|
1253
|
+
cell.appendChild(indicator);
|
|
1254
|
+
cell.addEventListener("click", (evt) => {
|
|
1255
|
+
const target = evt.target;
|
|
1256
|
+
if (target.closest(".gantt-col-resize-handle")) return;
|
|
1257
|
+
this.options.onSortClick?.(col.id);
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1196
1260
|
if (this.options.onColumnReorder) {
|
|
1197
1261
|
cell.draggable = true;
|
|
1198
1262
|
cell.style.cursor = "grab";
|
|
@@ -1319,8 +1383,8 @@ var GanttRenderer = class {
|
|
|
1319
1383
|
renderTimeline(model, tasks) {
|
|
1320
1384
|
this.svg.replaceChildren();
|
|
1321
1385
|
this.svg.setAttribute("width", String(model.width));
|
|
1322
|
-
this.svg.setAttribute("height", String(model.
|
|
1323
|
-
this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.
|
|
1386
|
+
this.svg.setAttribute("height", String(model.height));
|
|
1387
|
+
this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.height}`);
|
|
1324
1388
|
const defs = svgEl("defs");
|
|
1325
1389
|
const marker = svgEl("marker");
|
|
1326
1390
|
marker.setAttribute("id", this.markerDefsId);
|
|
@@ -1337,7 +1401,6 @@ var GanttRenderer = class {
|
|
|
1337
1401
|
this.svg.appendChild(defs);
|
|
1338
1402
|
const { startY, endY } = this.getViewportRowRange(model);
|
|
1339
1403
|
const bodyGroup = svgEl("g");
|
|
1340
|
-
bodyGroup.setAttribute("transform", `translate(0, ${model.headerHeight})`);
|
|
1341
1404
|
this.renderGridLines(bodyGroup, model);
|
|
1342
1405
|
this.renderRowBackgrounds(bodyGroup, model, startY, endY);
|
|
1343
1406
|
this.renderBars(bodyGroup, model, tasks, startY, endY);
|
|
@@ -1421,9 +1484,12 @@ var GanttRenderer = class {
|
|
|
1421
1484
|
});
|
|
1422
1485
|
}
|
|
1423
1486
|
renderHeader(model) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1487
|
+
this.headerSvg.replaceChildren();
|
|
1488
|
+
this.headerSvg.setAttribute("width", String(model.width));
|
|
1489
|
+
this.headerSvg.setAttribute("height", String(model.headerHeight));
|
|
1490
|
+
this.headerSvg.setAttribute("viewBox", `0 0 ${model.width} ${model.headerHeight}`);
|
|
1491
|
+
this.headerContainer.style.height = `${model.headerHeight}px`;
|
|
1492
|
+
const headerGroup = svgEl("g");
|
|
1427
1493
|
headerGroup.setAttribute("class", "gantt-header-group");
|
|
1428
1494
|
const bg = svgEl("rect");
|
|
1429
1495
|
bg.setAttribute("x", "0");
|
|
@@ -1458,7 +1524,7 @@ var GanttRenderer = class {
|
|
|
1458
1524
|
headerGroup.appendChild(text);
|
|
1459
1525
|
}
|
|
1460
1526
|
headerGroup.insertBefore(defs, headerGroup.firstChild);
|
|
1461
|
-
this.
|
|
1527
|
+
this.headerSvg.appendChild(headerGroup);
|
|
1462
1528
|
}
|
|
1463
1529
|
/**
|
|
1464
1530
|
* Invisible full-width hit-test rects, one per row, behind the bars. Lets
|
|
@@ -1493,11 +1559,6 @@ var GanttRenderer = class {
|
|
|
1493
1559
|
g.setAttribute("class", "gantt-bar");
|
|
1494
1560
|
g.dataset.ganttBar = bar.taskId;
|
|
1495
1561
|
g.setAttribute("transform", `translate(${bar.x}, ${bar.y})`);
|
|
1496
|
-
if (task?.notes) {
|
|
1497
|
-
const title = svgEl("title");
|
|
1498
|
-
title.textContent = task.notes;
|
|
1499
|
-
g.appendChild(title);
|
|
1500
|
-
}
|
|
1501
1562
|
if (this.options.keyboardAccessible) {
|
|
1502
1563
|
g.setAttribute("tabindex", "0");
|
|
1503
1564
|
g.setAttribute("role", "button");
|
|
@@ -1742,8 +1803,30 @@ var GanttRenderer = class {
|
|
|
1742
1803
|
group.appendChild(path);
|
|
1743
1804
|
}
|
|
1744
1805
|
}
|
|
1806
|
+
/**
|
|
1807
|
+
* The header and body are separate DOM elements at runtime (so plain CSS `position: sticky`
|
|
1808
|
+
* can pin the header - see the constructor's doc comment), but exports need one self-contained
|
|
1809
|
+
* SVG with both, laid out exactly as the original single-SVG version was.
|
|
1810
|
+
*/
|
|
1745
1811
|
toSVGString() {
|
|
1746
|
-
|
|
1812
|
+
const headerHeight = this.lastModel?.headerHeight ?? 0;
|
|
1813
|
+
const width = this.lastModel?.width ?? 0;
|
|
1814
|
+
const bodyHeight = this.lastModel?.height ?? 0;
|
|
1815
|
+
const combined = svgEl("svg");
|
|
1816
|
+
combined.setAttribute("xmlns", SVG_NS);
|
|
1817
|
+
combined.setAttribute("width", String(width));
|
|
1818
|
+
combined.setAttribute("height", String(headerHeight + bodyHeight));
|
|
1819
|
+
combined.setAttribute("viewBox", `0 0 ${width} ${headerHeight + bodyHeight}`);
|
|
1820
|
+
for (const child of Array.from(this.headerSvg.children)) {
|
|
1821
|
+
combined.appendChild(child.cloneNode(true));
|
|
1822
|
+
}
|
|
1823
|
+
const bodyGroup = svgEl("g");
|
|
1824
|
+
bodyGroup.setAttribute("transform", `translate(0, ${headerHeight})`);
|
|
1825
|
+
for (const child of Array.from(this.svg.children)) {
|
|
1826
|
+
bodyGroup.appendChild(child.cloneNode(true));
|
|
1827
|
+
}
|
|
1828
|
+
combined.appendChild(bodyGroup);
|
|
1829
|
+
return new XMLSerializer().serializeToString(combined);
|
|
1747
1830
|
}
|
|
1748
1831
|
destroy() {
|
|
1749
1832
|
if (this.scrollListener) {
|
|
@@ -2122,13 +2205,17 @@ var InteractionController = class {
|
|
|
2122
2205
|
}
|
|
2123
2206
|
};
|
|
2124
2207
|
this.onSvgDoubleClick = (evt) => {
|
|
2125
|
-
if (!this.callbacks.onLinkDblClick) return;
|
|
2126
2208
|
const target = evt.target;
|
|
2127
2209
|
const linkPath2 = target.closest("[data-gantt-link]");
|
|
2128
2210
|
const key = linkPath2?.dataset.ganttLink;
|
|
2129
|
-
if (
|
|
2130
|
-
|
|
2131
|
-
|
|
2211
|
+
if (key && this.callbacks.onLinkDblClick) {
|
|
2212
|
+
const [fromId, toId] = key.split("->");
|
|
2213
|
+
if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const barGroup = target.closest("[data-gantt-bar]");
|
|
2217
|
+
const taskId = barGroup?.dataset.ganttBar;
|
|
2218
|
+
if (taskId && this.callbacks.onBarDblClick) this.callbacks.onBarDblClick(taskId);
|
|
2132
2219
|
};
|
|
2133
2220
|
this.onPointerDown = (evt) => {
|
|
2134
2221
|
const target = evt.target;
|
|
@@ -2391,6 +2478,119 @@ var InteractionController = class {
|
|
|
2391
2478
|
}
|
|
2392
2479
|
};
|
|
2393
2480
|
|
|
2481
|
+
// src/tooltip.ts
|
|
2482
|
+
function formatDate(d) {
|
|
2483
|
+
return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
|
2484
|
+
}
|
|
2485
|
+
function defaultTooltipContent(task) {
|
|
2486
|
+
const el2 = document.createElement("div");
|
|
2487
|
+
const title = document.createElement("div");
|
|
2488
|
+
title.className = "gantt-tooltip-title";
|
|
2489
|
+
title.textContent = task.name;
|
|
2490
|
+
el2.appendChild(title);
|
|
2491
|
+
const dates = document.createElement("div");
|
|
2492
|
+
dates.className = "gantt-tooltip-row";
|
|
2493
|
+
dates.textContent = task.isMilestone ? formatDate(task.start) : `${formatDate(task.start)} \u2192 ${formatDate(task.end)}`;
|
|
2494
|
+
el2.appendChild(dates);
|
|
2495
|
+
if (task.progress !== void 0) {
|
|
2496
|
+
const progress = document.createElement("div");
|
|
2497
|
+
progress.className = "gantt-tooltip-row";
|
|
2498
|
+
progress.textContent = `${Math.round(task.progress)}% complete`;
|
|
2499
|
+
el2.appendChild(progress);
|
|
2500
|
+
}
|
|
2501
|
+
if (task.assignees && task.assignees.length > 0) {
|
|
2502
|
+
const assignees = document.createElement("div");
|
|
2503
|
+
assignees.className = "gantt-tooltip-row";
|
|
2504
|
+
assignees.textContent = task.assignees.map((a) => a.name).join(", ");
|
|
2505
|
+
el2.appendChild(assignees);
|
|
2506
|
+
}
|
|
2507
|
+
if (task.notes) {
|
|
2508
|
+
const notes = document.createElement("div");
|
|
2509
|
+
notes.className = "gantt-tooltip-notes";
|
|
2510
|
+
notes.textContent = task.notes;
|
|
2511
|
+
el2.appendChild(notes);
|
|
2512
|
+
}
|
|
2513
|
+
return el2;
|
|
2514
|
+
}
|
|
2515
|
+
var Tooltip = class {
|
|
2516
|
+
constructor(getOptions, getTask) {
|
|
2517
|
+
this.svg = null;
|
|
2518
|
+
this.hoveredTaskId = null;
|
|
2519
|
+
this.onPointerOver = (evt) => {
|
|
2520
|
+
if (!this.getOptions().enabled) return;
|
|
2521
|
+
const target = evt.target;
|
|
2522
|
+
const barGroup = target.closest("[data-gantt-bar]");
|
|
2523
|
+
const taskId = barGroup?.dataset.ganttBar;
|
|
2524
|
+
if (!taskId || taskId === this.hoveredTaskId) return;
|
|
2525
|
+
const task = this.getTask(taskId);
|
|
2526
|
+
if (!task) return;
|
|
2527
|
+
const { render } = this.getOptions();
|
|
2528
|
+
const content = render ? render(task) : defaultTooltipContent(task);
|
|
2529
|
+
if (content === null || content === void 0) {
|
|
2530
|
+
this.hide();
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
this.el.replaceChildren();
|
|
2534
|
+
if (content instanceof HTMLElement) {
|
|
2535
|
+
this.el.appendChild(content);
|
|
2536
|
+
} else {
|
|
2537
|
+
this.el.textContent = content;
|
|
2538
|
+
}
|
|
2539
|
+
this.hoveredTaskId = taskId;
|
|
2540
|
+
this.el.style.display = "block";
|
|
2541
|
+
this.position(evt.clientX, evt.clientY);
|
|
2542
|
+
};
|
|
2543
|
+
this.onPointerMove = (evt) => {
|
|
2544
|
+
if (this.hoveredTaskId === null) return;
|
|
2545
|
+
this.position(evt.clientX, evt.clientY);
|
|
2546
|
+
};
|
|
2547
|
+
this.onPointerOut = (evt) => {
|
|
2548
|
+
const related = evt.relatedTarget;
|
|
2549
|
+
const target = evt.target;
|
|
2550
|
+
const leftBarGroup = target.closest("[data-gantt-bar]");
|
|
2551
|
+
if (related && leftBarGroup?.contains(related)) return;
|
|
2552
|
+
this.hide();
|
|
2553
|
+
};
|
|
2554
|
+
this.getOptions = getOptions;
|
|
2555
|
+
this.getTask = getTask;
|
|
2556
|
+
this.el = document.createElement("div");
|
|
2557
|
+
this.el.className = "gantt-tooltip";
|
|
2558
|
+
this.el.setAttribute("role", "tooltip");
|
|
2559
|
+
this.el.style.display = "none";
|
|
2560
|
+
document.body.appendChild(this.el);
|
|
2561
|
+
}
|
|
2562
|
+
attach(svg) {
|
|
2563
|
+
this.svg = svg;
|
|
2564
|
+
svg.addEventListener("pointerover", this.onPointerOver);
|
|
2565
|
+
svg.addEventListener("pointermove", this.onPointerMove);
|
|
2566
|
+
svg.addEventListener("pointerout", this.onPointerOut);
|
|
2567
|
+
}
|
|
2568
|
+
position(clientX, clientY) {
|
|
2569
|
+
const OFFSET = 12;
|
|
2570
|
+
const rect = this.el.getBoundingClientRect();
|
|
2571
|
+
const viewportW = window.innerWidth;
|
|
2572
|
+
const viewportH = window.innerHeight;
|
|
2573
|
+
let left = clientX + OFFSET;
|
|
2574
|
+
let top = clientY + OFFSET;
|
|
2575
|
+
if (left + rect.width > viewportW) left = clientX - OFFSET - rect.width;
|
|
2576
|
+
if (top + rect.height > viewportH) top = clientY - OFFSET - rect.height;
|
|
2577
|
+
this.el.style.left = `${Math.max(0, left)}px`;
|
|
2578
|
+
this.el.style.top = `${Math.max(0, top)}px`;
|
|
2579
|
+
}
|
|
2580
|
+
hide() {
|
|
2581
|
+
this.hoveredTaskId = null;
|
|
2582
|
+
this.el.style.display = "none";
|
|
2583
|
+
}
|
|
2584
|
+
destroy() {
|
|
2585
|
+
if (this.svg) {
|
|
2586
|
+
this.svg.removeEventListener("pointerover", this.onPointerOver);
|
|
2587
|
+
this.svg.removeEventListener("pointermove", this.onPointerMove);
|
|
2588
|
+
this.svg.removeEventListener("pointerout", this.onPointerOut);
|
|
2589
|
+
}
|
|
2590
|
+
this.el.remove();
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
|
|
2394
2594
|
// src/index.ts
|
|
2395
2595
|
var HAS_DOM = typeof document !== "undefined" && typeof window !== "undefined";
|
|
2396
2596
|
var DEFAULT_COLUMNS = [{ id: "name", title: "Name" }];
|
|
@@ -2410,6 +2610,9 @@ var _GanttChart = class _GanttChart {
|
|
|
2410
2610
|
this.history = null;
|
|
2411
2611
|
this.renderer = null;
|
|
2412
2612
|
this.interactions = null;
|
|
2613
|
+
this.tooltip = null;
|
|
2614
|
+
this.resizeObserver = null;
|
|
2615
|
+
this.sort = null;
|
|
2413
2616
|
this.renderModel = null;
|
|
2414
2617
|
this.rafHandle = null;
|
|
2415
2618
|
this.currentPage = 1;
|
|
@@ -2432,6 +2635,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2432
2635
|
showCriticalPath: options.showCriticalPath ?? false,
|
|
2433
2636
|
showBaseline: options.showBaseline ?? false,
|
|
2434
2637
|
showDeadlines: options.showDeadlines ?? true,
|
|
2638
|
+
showTooltip: options.showTooltip ?? true,
|
|
2639
|
+
autoFitToViewport: options.autoFitToViewport ?? false,
|
|
2435
2640
|
showAssigneeAvatars: options.showAssigneeAvatars ?? false,
|
|
2436
2641
|
enableHistory: options.enableHistory ?? false,
|
|
2437
2642
|
keyboardAccessible: options.keyboardAccessible ?? false,
|
|
@@ -2452,13 +2657,16 @@ var _GanttChart = class _GanttChart {
|
|
|
2452
2657
|
onDependencyRemove: options.onDependencyRemove,
|
|
2453
2658
|
onDependencyDblClick: options.onDependencyDblClick,
|
|
2454
2659
|
onTaskClick: options.onTaskClick,
|
|
2660
|
+
onTaskDblClick: options.onTaskDblClick,
|
|
2455
2661
|
onGroupToggle: options.onGroupToggle,
|
|
2456
2662
|
onContextMenu: options.onContextMenu,
|
|
2457
2663
|
onTaskCreate: options.onTaskCreate,
|
|
2458
2664
|
onColumnResize: options.onColumnResize,
|
|
2459
2665
|
onColumnReorder: options.onColumnReorder,
|
|
2460
2666
|
onTaskReorder: options.onTaskReorder,
|
|
2461
|
-
onSelectionChange: options.onSelectionChange
|
|
2667
|
+
onSelectionChange: options.onSelectionChange,
|
|
2668
|
+
onSortChange: options.onSortChange,
|
|
2669
|
+
renderTooltip: options.renderTooltip
|
|
2462
2670
|
};
|
|
2463
2671
|
if (this.options.enableHistory) {
|
|
2464
2672
|
this.history = new HistoryManager((state) => this.emitter.emit("history-change", state));
|
|
@@ -2481,7 +2689,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2481
2689
|
onRenameCommit: (taskId, name) => this.updateTask(taskId, { name }),
|
|
2482
2690
|
onColumnResize: this.options.onColumnResize ? (columnId, width) => this.handleColumnResize(columnId, width) : void 0,
|
|
2483
2691
|
onColumnReorder: this.options.onColumnReorder ? (order) => this.handleColumnReorder(order) : void 0,
|
|
2484
|
-
onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0
|
|
2692
|
+
onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
|
|
2693
|
+
onSortClick: (columnId) => this.handleSortClick(columnId)
|
|
2485
2694
|
});
|
|
2486
2695
|
this.interactions = new InteractionController(
|
|
2487
2696
|
this.renderer.svg,
|
|
@@ -2502,7 +2711,17 @@ var _GanttChart = class _GanttChart {
|
|
|
2502
2711
|
onCreateTaskDrag: this.options.onTaskCreate ? (rowTaskId, startXPx, endXPx) => this.handleCreateTaskDrag(rowTaskId, startXPx, endXPx) : void 0,
|
|
2503
2712
|
onLinkDblClick: this.options.onDependencyDblClick ? (fromId, toId) => {
|
|
2504
2713
|
const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
|
|
2505
|
-
if (dep)
|
|
2714
|
+
if (dep) {
|
|
2715
|
+
this.emitter.emit("dependency-dblclick", dep);
|
|
2716
|
+
this.options.onDependencyDblClick?.(dep);
|
|
2717
|
+
}
|
|
2718
|
+
} : void 0,
|
|
2719
|
+
onBarDblClick: this.options.onTaskDblClick ? (taskId) => {
|
|
2720
|
+
const task = this.tasks.find((t) => t.id === taskId);
|
|
2721
|
+
if (task) {
|
|
2722
|
+
this.emitter.emit("task-dblclick", { task });
|
|
2723
|
+
this.options.onTaskDblClick?.(task);
|
|
2724
|
+
}
|
|
2506
2725
|
} : void 0,
|
|
2507
2726
|
onContextMenu: this.options.onContextMenu ? (taskId, evt) => {
|
|
2508
2727
|
const task = this.tasks.find((t) => t.id === taskId);
|
|
@@ -2521,6 +2740,17 @@ var _GanttChart = class _GanttChart {
|
|
|
2521
2740
|
pxPerMs: () => pxPerMs(this.options.viewMode, this.columnWidth)
|
|
2522
2741
|
}
|
|
2523
2742
|
);
|
|
2743
|
+
this.tooltip = new Tooltip(
|
|
2744
|
+
() => ({ enabled: this.options.showTooltip, render: this.options.renderTooltip }),
|
|
2745
|
+
(taskId) => this.tasks.find((t) => t.id === taskId)
|
|
2746
|
+
);
|
|
2747
|
+
this.tooltip.attach(this.renderer.svg);
|
|
2748
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
2749
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
2750
|
+
if (this.options.autoFitToViewport) this.scheduleRender();
|
|
2751
|
+
});
|
|
2752
|
+
this.resizeObserver.observe(this.container);
|
|
2753
|
+
}
|
|
2524
2754
|
}
|
|
2525
2755
|
handleMove(taskId, dxMs) {
|
|
2526
2756
|
if (dxMs === 0) return;
|
|
@@ -2925,6 +3155,24 @@ var _GanttChart = class _GanttChart {
|
|
|
2925
3155
|
this.options.onColumnReorder?.(order);
|
|
2926
3156
|
this.scheduleRender();
|
|
2927
3157
|
}
|
|
3158
|
+
/** Cycles a sortable column's header through asc -> desc -> none; clicking a different column starts it fresh at asc. */
|
|
3159
|
+
handleSortClick(columnId) {
|
|
3160
|
+
if (this.sort?.columnId === columnId) {
|
|
3161
|
+
this.setSort(columnId, this.sort.direction === "asc" ? "desc" : null);
|
|
3162
|
+
} else {
|
|
3163
|
+
this.setSort(columnId, "asc");
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
/** Sorts siblings at every tree level by a column's value (see GanttColumn.sortable); `direction: null`/omitted `columnId` clears it. */
|
|
3167
|
+
setSort(columnId, direction = "asc") {
|
|
3168
|
+
this.sort = columnId && direction ? { columnId, direction } : null;
|
|
3169
|
+
this.emitter.emit("sort-change", { sort: this.sort });
|
|
3170
|
+
this.options.onSortChange?.(this.sort);
|
|
3171
|
+
this.scheduleRender();
|
|
3172
|
+
}
|
|
3173
|
+
getSort() {
|
|
3174
|
+
return this.sort;
|
|
3175
|
+
}
|
|
2928
3176
|
handleRowReorder(draggedId, targetId, position) {
|
|
2929
3177
|
const draggedIndex = this.tasks.findIndex((t) => t.id === draggedId);
|
|
2930
3178
|
const targetIndex = this.tasks.findIndex((t) => t.id === targetId);
|
|
@@ -2991,13 +3239,18 @@ var _GanttChart = class _GanttChart {
|
|
|
2991
3239
|
this.scheduleRender();
|
|
2992
3240
|
}
|
|
2993
3241
|
setOptions(partial) {
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
if (
|
|
3242
|
+
const hasTheme = "theme" in partial;
|
|
3243
|
+
const hasColorScheme = "colorScheme" in partial;
|
|
3244
|
+
if (hasColorScheme) this.colorScheme = partial.colorScheme ?? "auto";
|
|
3245
|
+
if (hasTheme) this.explicitTheme = partial.theme;
|
|
3246
|
+
if (hasTheme || hasColorScheme) {
|
|
2997
3247
|
this.theme = mergeTheme(this.explicitTheme, this.colorScheme);
|
|
2998
3248
|
}
|
|
2999
|
-
if (partial
|
|
3249
|
+
if ("columns" in partial) this.columns = partial.columns ?? this.columns;
|
|
3000
3250
|
if (partial.columnWidth !== void 0) this.columnWidth = partial.columnWidth;
|
|
3251
|
+
if (partial.gridPanelWidth !== void 0) {
|
|
3252
|
+
this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
|
|
3253
|
+
}
|
|
3001
3254
|
Object.assign(this.options, partial);
|
|
3002
3255
|
this.scheduleRender();
|
|
3003
3256
|
}
|
|
@@ -3100,7 +3353,8 @@ var _GanttChart = class _GanttChart {
|
|
|
3100
3353
|
markers: this.options.markers,
|
|
3101
3354
|
autoRollupProgress: this.options.autoRollupProgress,
|
|
3102
3355
|
selectedTaskIds: this.options.selectable ? this.selectedTaskIds : void 0,
|
|
3103
|
-
pagination
|
|
3356
|
+
pagination,
|
|
3357
|
+
sort: this.sort
|
|
3104
3358
|
});
|
|
3105
3359
|
}
|
|
3106
3360
|
scheduleRender(immediate = false) {
|
|
@@ -3115,6 +3369,7 @@ var _GanttChart = class _GanttChart {
|
|
|
3115
3369
|
});
|
|
3116
3370
|
}
|
|
3117
3371
|
doRender() {
|
|
3372
|
+
if (this.options.autoFitToViewport) this.applyAutoFit();
|
|
3118
3373
|
this.renderModel = this.computeModel();
|
|
3119
3374
|
this.renderer?.render(this.renderModel, this.tasks, this.columns, this.explicitTheme);
|
|
3120
3375
|
if (this.renderer && this.colorScheme !== "auto") {
|
|
@@ -3123,6 +3378,29 @@ var _GanttChart = class _GanttChart {
|
|
|
3123
3378
|
this.renderer?.root.removeAttribute("data-gantt-theme");
|
|
3124
3379
|
}
|
|
3125
3380
|
}
|
|
3381
|
+
/**
|
|
3382
|
+
* Stretch-only fit: bump columnWidth up so the timeline fills the container's available
|
|
3383
|
+
* width, but never shrink it below what the current zoom level already implies - a project
|
|
3384
|
+
* wider than the container should still scroll normally, not get crammed to fit.
|
|
3385
|
+
*/
|
|
3386
|
+
applyAutoFit() {
|
|
3387
|
+
const availableWidth = this.container.clientWidth - this.options.gridPanelWidth;
|
|
3388
|
+
if (!Number.isFinite(availableWidth) || availableWidth <= 0) return;
|
|
3389
|
+
let min = null;
|
|
3390
|
+
let max = null;
|
|
3391
|
+
for (const t of this.tasks) {
|
|
3392
|
+
if (!isValidDate(t.start) || !isValidDate(t.end)) continue;
|
|
3393
|
+
min = min === null ? t.start.getTime() : Math.min(min, t.start.getTime());
|
|
3394
|
+
max = max === null ? t.end.getTime() : Math.max(max, t.end.getTime());
|
|
3395
|
+
}
|
|
3396
|
+
if (min === null || max === null || max <= min) return;
|
|
3397
|
+
const rangeMs = max - min;
|
|
3398
|
+
const unitMs = approxUnitMs(this.options.viewMode);
|
|
3399
|
+
const neededColumnWidth = availableWidth * unitMs / rangeMs;
|
|
3400
|
+
if (neededColumnWidth > this.columnWidth) {
|
|
3401
|
+
this.columnWidth = Math.min(neededColumnWidth, _GanttChart.MAX_COLUMN_WIDTH);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3126
3404
|
undo() {
|
|
3127
3405
|
this.history?.undo();
|
|
3128
3406
|
}
|
|
@@ -3217,6 +3495,8 @@ var _GanttChart = class _GanttChart {
|
|
|
3217
3495
|
this.darkMediaQuery?.removeEventListener?.("change", this.handleSchemeChange);
|
|
3218
3496
|
this.emitter.removeAllListeners();
|
|
3219
3497
|
this.interactions?.destroy();
|
|
3498
|
+
this.tooltip?.destroy();
|
|
3499
|
+
this.resizeObserver?.disconnect();
|
|
3220
3500
|
this.renderer?.destroy();
|
|
3221
3501
|
}
|
|
3222
3502
|
};
|