@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.cjs
CHANGED
|
@@ -663,6 +663,29 @@ function computeProgressRollup(node) {
|
|
|
663
663
|
}
|
|
664
664
|
node.computedProgress = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
665
665
|
}
|
|
666
|
+
function sortValue(node, col) {
|
|
667
|
+
if (!col) return null;
|
|
668
|
+
if (col.accessor) return col.accessor(node.task) ?? null;
|
|
669
|
+
return col.id === "name" ? node.task.name : null;
|
|
670
|
+
}
|
|
671
|
+
function compareValues(a, b) {
|
|
672
|
+
if (a === null && b === null) return 0;
|
|
673
|
+
if (a === null) return 1;
|
|
674
|
+
if (b === null) return -1;
|
|
675
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
676
|
+
return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
|
|
677
|
+
}
|
|
678
|
+
function sortTree(roots, columns, sort) {
|
|
679
|
+
if (!sort) return;
|
|
680
|
+
const col = columns.find((c) => c.id === sort.columnId);
|
|
681
|
+
const dir = sort.direction === "desc" ? -1 : 1;
|
|
682
|
+
const compare = (a, b) => dir * compareValues(sortValue(a, col), sortValue(b, col));
|
|
683
|
+
const sortLevel = (nodes) => {
|
|
684
|
+
nodes.sort(compare);
|
|
685
|
+
for (const node of nodes) sortLevel(node.children);
|
|
686
|
+
};
|
|
687
|
+
sortLevel(roots);
|
|
688
|
+
}
|
|
666
689
|
function flatten(roots) {
|
|
667
690
|
const out = [];
|
|
668
691
|
const stack = [];
|
|
@@ -749,6 +772,14 @@ function linkPath(from, to) {
|
|
|
749
772
|
const c2y = to.y;
|
|
750
773
|
return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
|
|
751
774
|
}
|
|
775
|
+
var MIN_VISIBLE_UNITS = {
|
|
776
|
+
hour: 8,
|
|
777
|
+
day: 7,
|
|
778
|
+
week: 6,
|
|
779
|
+
month: 6,
|
|
780
|
+
quarter: 4,
|
|
781
|
+
year: 5
|
|
782
|
+
};
|
|
752
783
|
function computeLayout(input) {
|
|
753
784
|
const {
|
|
754
785
|
tasks,
|
|
@@ -764,7 +795,8 @@ function computeLayout(input) {
|
|
|
764
795
|
markers = [],
|
|
765
796
|
autoRollupProgress = false,
|
|
766
797
|
selectedTaskIds,
|
|
767
|
-
pagination
|
|
798
|
+
pagination,
|
|
799
|
+
sort = null
|
|
768
800
|
} = input;
|
|
769
801
|
validateInputs(tasks, dependencies);
|
|
770
802
|
const { roots, nodeById } = buildTree(tasks);
|
|
@@ -772,6 +804,7 @@ function computeLayout(input) {
|
|
|
772
804
|
if (autoRollupProgress) {
|
|
773
805
|
for (const root of roots) computeProgressRollup(root);
|
|
774
806
|
}
|
|
807
|
+
sortTree(roots, columns, sort);
|
|
775
808
|
let visibleRoots = roots;
|
|
776
809
|
if (pagination && pagination.pageSize > 0) {
|
|
777
810
|
const start = Math.max(0, (pagination.page - 1) * pagination.pageSize);
|
|
@@ -791,8 +824,15 @@ function computeLayout(input) {
|
|
|
791
824
|
const now = Date.now();
|
|
792
825
|
const rawRangeStart = rangeMin === null ? new Date(now) : new Date(rangeMin);
|
|
793
826
|
const rawRangeEnd = rangeMax === null ? new Date(now + 1) : new Date(rangeMax);
|
|
794
|
-
|
|
795
|
-
|
|
827
|
+
let paddedStart = addUnit(rawRangeStart, viewMode, -1);
|
|
828
|
+
let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
|
|
829
|
+
const minUnits = MIN_VISIBLE_UNITS[viewMode];
|
|
830
|
+
const currentUnits = (paddedEnd.getTime() - paddedStart.getTime()) / approxUnitMs(viewMode);
|
|
831
|
+
if (currentUnits < minUnits) {
|
|
832
|
+
const deficit = minUnits - currentUnits;
|
|
833
|
+
paddedStart = addUnit(paddedStart, viewMode, -Math.ceil(deficit / 2));
|
|
834
|
+
paddedEnd = addUnit(paddedEnd, viewMode, Math.floor(deficit / 2));
|
|
835
|
+
}
|
|
796
836
|
const rows = [];
|
|
797
837
|
const bars = [];
|
|
798
838
|
const barByTaskId = /* @__PURE__ */ new Map();
|
|
@@ -943,7 +983,8 @@ function computeLayout(input) {
|
|
|
943
983
|
columns,
|
|
944
984
|
theme,
|
|
945
985
|
markers: renderMarkers,
|
|
946
|
-
rangeStart: paddedStart
|
|
986
|
+
rangeStart: paddedStart,
|
|
987
|
+
sort
|
|
947
988
|
};
|
|
948
989
|
}
|
|
949
990
|
|
|
@@ -1140,6 +1181,7 @@ var GanttRenderer = class {
|
|
|
1140
1181
|
this.lastModel = null;
|
|
1141
1182
|
this.lastTasks = [];
|
|
1142
1183
|
this.lastColumns = [];
|
|
1184
|
+
this.lastExplicitThemeKeys = /* @__PURE__ */ new Set();
|
|
1143
1185
|
this.onGridDoubleClick = (evt) => {
|
|
1144
1186
|
if (!this.options.onRenameCommit) return;
|
|
1145
1187
|
const target = evt.target;
|
|
@@ -1177,8 +1219,13 @@ var GanttRenderer = class {
|
|
|
1177
1219
|
this.root = el("div", "gantt-root");
|
|
1178
1220
|
this.gridPanel = el("div", "gantt-grid-panel");
|
|
1179
1221
|
this.timelineScroll = el("div", "gantt-timeline-scroll");
|
|
1222
|
+
this.headerContainer = el("div", "gantt-timeline-header");
|
|
1223
|
+
this.headerSvg = svgEl("svg");
|
|
1224
|
+
this.headerSvg.classList.add("gantt-header-svg");
|
|
1225
|
+
this.headerContainer.appendChild(this.headerSvg);
|
|
1180
1226
|
this.svg = svgEl("svg");
|
|
1181
1227
|
this.svg.classList.add("gantt-svg");
|
|
1228
|
+
this.timelineScroll.appendChild(this.headerContainer);
|
|
1182
1229
|
this.timelineScroll.appendChild(this.svg);
|
|
1183
1230
|
this.root.appendChild(this.gridPanel);
|
|
1184
1231
|
this.root.appendChild(this.timelineScroll);
|
|
@@ -1194,9 +1241,14 @@ var GanttRenderer = class {
|
|
|
1194
1241
|
this.lastTasks = tasks;
|
|
1195
1242
|
this.lastColumns = columns;
|
|
1196
1243
|
const cssVars = explicitThemeToCssVars(explicitTheme);
|
|
1197
|
-
|
|
1244
|
+
const newKeys = new Set(Object.keys(cssVars));
|
|
1245
|
+
for (const key of this.lastExplicitThemeKeys) {
|
|
1246
|
+
if (!newKeys.has(key)) this.root.style.removeProperty(key);
|
|
1247
|
+
}
|
|
1248
|
+
for (const key of newKeys) {
|
|
1198
1249
|
this.root.style.setProperty(key, cssVars[key]);
|
|
1199
1250
|
}
|
|
1251
|
+
this.lastExplicitThemeKeys = newKeys;
|
|
1200
1252
|
this.renderGridPanel(model, tasks, columns);
|
|
1201
1253
|
this.renderTimeline(model, tasks);
|
|
1202
1254
|
}
|
|
@@ -1224,7 +1276,7 @@ var GanttRenderer = class {
|
|
|
1224
1276
|
const header = el("div", "gantt-grid-header-row");
|
|
1225
1277
|
header.style.height = `${model.headerHeight}px`;
|
|
1226
1278
|
for (const col of columns) {
|
|
1227
|
-
header.appendChild(this.renderHeaderCell(col, columns));
|
|
1279
|
+
header.appendChild(this.renderHeaderCell(col, columns, model.sort));
|
|
1228
1280
|
}
|
|
1229
1281
|
this.gridPanel.appendChild(header);
|
|
1230
1282
|
const body = el("div", "gantt-grid-body");
|
|
@@ -1237,11 +1289,23 @@ var GanttRenderer = class {
|
|
|
1237
1289
|
});
|
|
1238
1290
|
this.gridPanel.appendChild(body);
|
|
1239
1291
|
}
|
|
1240
|
-
renderHeaderCell(col, columns) {
|
|
1292
|
+
renderHeaderCell(col, columns, sort) {
|
|
1241
1293
|
const cell = el("div", "gantt-grid-header-cell");
|
|
1242
1294
|
cell.style.position = "relative";
|
|
1243
1295
|
if (col.width) cell.style.width = `${col.width}px`;
|
|
1244
1296
|
cell.textContent = col.title;
|
|
1297
|
+
if (col.sortable) {
|
|
1298
|
+
cell.classList.add("gantt-grid-header-cell-sortable");
|
|
1299
|
+
const active = sort?.columnId === col.id;
|
|
1300
|
+
const indicator = el("span", "gantt-sort-indicator");
|
|
1301
|
+
indicator.textContent = active ? sort.direction === "asc" ? " \u25B2" : " \u25BC" : "";
|
|
1302
|
+
cell.appendChild(indicator);
|
|
1303
|
+
cell.addEventListener("click", (evt) => {
|
|
1304
|
+
const target = evt.target;
|
|
1305
|
+
if (target.closest(".gantt-col-resize-handle")) return;
|
|
1306
|
+
this.options.onSortClick?.(col.id);
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1245
1309
|
if (this.options.onColumnReorder) {
|
|
1246
1310
|
cell.draggable = true;
|
|
1247
1311
|
cell.style.cursor = "grab";
|
|
@@ -1368,8 +1432,8 @@ var GanttRenderer = class {
|
|
|
1368
1432
|
renderTimeline(model, tasks) {
|
|
1369
1433
|
this.svg.replaceChildren();
|
|
1370
1434
|
this.svg.setAttribute("width", String(model.width));
|
|
1371
|
-
this.svg.setAttribute("height", String(model.
|
|
1372
|
-
this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.
|
|
1435
|
+
this.svg.setAttribute("height", String(model.height));
|
|
1436
|
+
this.svg.setAttribute("viewBox", `0 0 ${model.width} ${model.height}`);
|
|
1373
1437
|
const defs = svgEl("defs");
|
|
1374
1438
|
const marker = svgEl("marker");
|
|
1375
1439
|
marker.setAttribute("id", this.markerDefsId);
|
|
@@ -1386,7 +1450,6 @@ var GanttRenderer = class {
|
|
|
1386
1450
|
this.svg.appendChild(defs);
|
|
1387
1451
|
const { startY, endY } = this.getViewportRowRange(model);
|
|
1388
1452
|
const bodyGroup = svgEl("g");
|
|
1389
|
-
bodyGroup.setAttribute("transform", `translate(0, ${model.headerHeight})`);
|
|
1390
1453
|
this.renderGridLines(bodyGroup, model);
|
|
1391
1454
|
this.renderRowBackgrounds(bodyGroup, model, startY, endY);
|
|
1392
1455
|
this.renderBars(bodyGroup, model, tasks, startY, endY);
|
|
@@ -1470,9 +1533,12 @@ var GanttRenderer = class {
|
|
|
1470
1533
|
});
|
|
1471
1534
|
}
|
|
1472
1535
|
renderHeader(model) {
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1536
|
+
this.headerSvg.replaceChildren();
|
|
1537
|
+
this.headerSvg.setAttribute("width", String(model.width));
|
|
1538
|
+
this.headerSvg.setAttribute("height", String(model.headerHeight));
|
|
1539
|
+
this.headerSvg.setAttribute("viewBox", `0 0 ${model.width} ${model.headerHeight}`);
|
|
1540
|
+
this.headerContainer.style.height = `${model.headerHeight}px`;
|
|
1541
|
+
const headerGroup = svgEl("g");
|
|
1476
1542
|
headerGroup.setAttribute("class", "gantt-header-group");
|
|
1477
1543
|
const bg = svgEl("rect");
|
|
1478
1544
|
bg.setAttribute("x", "0");
|
|
@@ -1507,7 +1573,7 @@ var GanttRenderer = class {
|
|
|
1507
1573
|
headerGroup.appendChild(text);
|
|
1508
1574
|
}
|
|
1509
1575
|
headerGroup.insertBefore(defs, headerGroup.firstChild);
|
|
1510
|
-
this.
|
|
1576
|
+
this.headerSvg.appendChild(headerGroup);
|
|
1511
1577
|
}
|
|
1512
1578
|
/**
|
|
1513
1579
|
* Invisible full-width hit-test rects, one per row, behind the bars. Lets
|
|
@@ -1542,11 +1608,6 @@ var GanttRenderer = class {
|
|
|
1542
1608
|
g.setAttribute("class", "gantt-bar");
|
|
1543
1609
|
g.dataset.ganttBar = bar.taskId;
|
|
1544
1610
|
g.setAttribute("transform", `translate(${bar.x}, ${bar.y})`);
|
|
1545
|
-
if (task?.notes) {
|
|
1546
|
-
const title = svgEl("title");
|
|
1547
|
-
title.textContent = task.notes;
|
|
1548
|
-
g.appendChild(title);
|
|
1549
|
-
}
|
|
1550
1611
|
if (this.options.keyboardAccessible) {
|
|
1551
1612
|
g.setAttribute("tabindex", "0");
|
|
1552
1613
|
g.setAttribute("role", "button");
|
|
@@ -1791,8 +1852,30 @@ var GanttRenderer = class {
|
|
|
1791
1852
|
group.appendChild(path);
|
|
1792
1853
|
}
|
|
1793
1854
|
}
|
|
1855
|
+
/**
|
|
1856
|
+
* The header and body are separate DOM elements at runtime (so plain CSS `position: sticky`
|
|
1857
|
+
* can pin the header - see the constructor's doc comment), but exports need one self-contained
|
|
1858
|
+
* SVG with both, laid out exactly as the original single-SVG version was.
|
|
1859
|
+
*/
|
|
1794
1860
|
toSVGString() {
|
|
1795
|
-
|
|
1861
|
+
const headerHeight = this.lastModel?.headerHeight ?? 0;
|
|
1862
|
+
const width = this.lastModel?.width ?? 0;
|
|
1863
|
+
const bodyHeight = this.lastModel?.height ?? 0;
|
|
1864
|
+
const combined = svgEl("svg");
|
|
1865
|
+
combined.setAttribute("xmlns", SVG_NS);
|
|
1866
|
+
combined.setAttribute("width", String(width));
|
|
1867
|
+
combined.setAttribute("height", String(headerHeight + bodyHeight));
|
|
1868
|
+
combined.setAttribute("viewBox", `0 0 ${width} ${headerHeight + bodyHeight}`);
|
|
1869
|
+
for (const child of Array.from(this.headerSvg.children)) {
|
|
1870
|
+
combined.appendChild(child.cloneNode(true));
|
|
1871
|
+
}
|
|
1872
|
+
const bodyGroup = svgEl("g");
|
|
1873
|
+
bodyGroup.setAttribute("transform", `translate(0, ${headerHeight})`);
|
|
1874
|
+
for (const child of Array.from(this.svg.children)) {
|
|
1875
|
+
bodyGroup.appendChild(child.cloneNode(true));
|
|
1876
|
+
}
|
|
1877
|
+
combined.appendChild(bodyGroup);
|
|
1878
|
+
return new XMLSerializer().serializeToString(combined);
|
|
1796
1879
|
}
|
|
1797
1880
|
destroy() {
|
|
1798
1881
|
if (this.scrollListener) {
|
|
@@ -2171,13 +2254,17 @@ var InteractionController = class {
|
|
|
2171
2254
|
}
|
|
2172
2255
|
};
|
|
2173
2256
|
this.onSvgDoubleClick = (evt) => {
|
|
2174
|
-
if (!this.callbacks.onLinkDblClick) return;
|
|
2175
2257
|
const target = evt.target;
|
|
2176
2258
|
const linkPath2 = target.closest("[data-gantt-link]");
|
|
2177
2259
|
const key = linkPath2?.dataset.ganttLink;
|
|
2178
|
-
if (
|
|
2179
|
-
|
|
2180
|
-
|
|
2260
|
+
if (key && this.callbacks.onLinkDblClick) {
|
|
2261
|
+
const [fromId, toId] = key.split("->");
|
|
2262
|
+
if (fromId && toId) this.callbacks.onLinkDblClick(fromId, toId);
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
const barGroup = target.closest("[data-gantt-bar]");
|
|
2266
|
+
const taskId = barGroup?.dataset.ganttBar;
|
|
2267
|
+
if (taskId && this.callbacks.onBarDblClick) this.callbacks.onBarDblClick(taskId);
|
|
2181
2268
|
};
|
|
2182
2269
|
this.onPointerDown = (evt) => {
|
|
2183
2270
|
const target = evt.target;
|
|
@@ -2440,6 +2527,119 @@ var InteractionController = class {
|
|
|
2440
2527
|
}
|
|
2441
2528
|
};
|
|
2442
2529
|
|
|
2530
|
+
// src/tooltip.ts
|
|
2531
|
+
function formatDate(d) {
|
|
2532
|
+
return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
|
2533
|
+
}
|
|
2534
|
+
function defaultTooltipContent(task) {
|
|
2535
|
+
const el2 = document.createElement("div");
|
|
2536
|
+
const title = document.createElement("div");
|
|
2537
|
+
title.className = "gantt-tooltip-title";
|
|
2538
|
+
title.textContent = task.name;
|
|
2539
|
+
el2.appendChild(title);
|
|
2540
|
+
const dates = document.createElement("div");
|
|
2541
|
+
dates.className = "gantt-tooltip-row";
|
|
2542
|
+
dates.textContent = task.isMilestone ? formatDate(task.start) : `${formatDate(task.start)} \u2192 ${formatDate(task.end)}`;
|
|
2543
|
+
el2.appendChild(dates);
|
|
2544
|
+
if (task.progress !== void 0) {
|
|
2545
|
+
const progress = document.createElement("div");
|
|
2546
|
+
progress.className = "gantt-tooltip-row";
|
|
2547
|
+
progress.textContent = `${Math.round(task.progress)}% complete`;
|
|
2548
|
+
el2.appendChild(progress);
|
|
2549
|
+
}
|
|
2550
|
+
if (task.assignees && task.assignees.length > 0) {
|
|
2551
|
+
const assignees = document.createElement("div");
|
|
2552
|
+
assignees.className = "gantt-tooltip-row";
|
|
2553
|
+
assignees.textContent = task.assignees.map((a) => a.name).join(", ");
|
|
2554
|
+
el2.appendChild(assignees);
|
|
2555
|
+
}
|
|
2556
|
+
if (task.notes) {
|
|
2557
|
+
const notes = document.createElement("div");
|
|
2558
|
+
notes.className = "gantt-tooltip-notes";
|
|
2559
|
+
notes.textContent = task.notes;
|
|
2560
|
+
el2.appendChild(notes);
|
|
2561
|
+
}
|
|
2562
|
+
return el2;
|
|
2563
|
+
}
|
|
2564
|
+
var Tooltip = class {
|
|
2565
|
+
constructor(getOptions, getTask) {
|
|
2566
|
+
this.svg = null;
|
|
2567
|
+
this.hoveredTaskId = null;
|
|
2568
|
+
this.onPointerOver = (evt) => {
|
|
2569
|
+
if (!this.getOptions().enabled) return;
|
|
2570
|
+
const target = evt.target;
|
|
2571
|
+
const barGroup = target.closest("[data-gantt-bar]");
|
|
2572
|
+
const taskId = barGroup?.dataset.ganttBar;
|
|
2573
|
+
if (!taskId || taskId === this.hoveredTaskId) return;
|
|
2574
|
+
const task = this.getTask(taskId);
|
|
2575
|
+
if (!task) return;
|
|
2576
|
+
const { render } = this.getOptions();
|
|
2577
|
+
const content = render ? render(task) : defaultTooltipContent(task);
|
|
2578
|
+
if (content === null || content === void 0) {
|
|
2579
|
+
this.hide();
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
this.el.replaceChildren();
|
|
2583
|
+
if (content instanceof HTMLElement) {
|
|
2584
|
+
this.el.appendChild(content);
|
|
2585
|
+
} else {
|
|
2586
|
+
this.el.textContent = content;
|
|
2587
|
+
}
|
|
2588
|
+
this.hoveredTaskId = taskId;
|
|
2589
|
+
this.el.style.display = "block";
|
|
2590
|
+
this.position(evt.clientX, evt.clientY);
|
|
2591
|
+
};
|
|
2592
|
+
this.onPointerMove = (evt) => {
|
|
2593
|
+
if (this.hoveredTaskId === null) return;
|
|
2594
|
+
this.position(evt.clientX, evt.clientY);
|
|
2595
|
+
};
|
|
2596
|
+
this.onPointerOut = (evt) => {
|
|
2597
|
+
const related = evt.relatedTarget;
|
|
2598
|
+
const target = evt.target;
|
|
2599
|
+
const leftBarGroup = target.closest("[data-gantt-bar]");
|
|
2600
|
+
if (related && leftBarGroup?.contains(related)) return;
|
|
2601
|
+
this.hide();
|
|
2602
|
+
};
|
|
2603
|
+
this.getOptions = getOptions;
|
|
2604
|
+
this.getTask = getTask;
|
|
2605
|
+
this.el = document.createElement("div");
|
|
2606
|
+
this.el.className = "gantt-tooltip";
|
|
2607
|
+
this.el.setAttribute("role", "tooltip");
|
|
2608
|
+
this.el.style.display = "none";
|
|
2609
|
+
document.body.appendChild(this.el);
|
|
2610
|
+
}
|
|
2611
|
+
attach(svg) {
|
|
2612
|
+
this.svg = svg;
|
|
2613
|
+
svg.addEventListener("pointerover", this.onPointerOver);
|
|
2614
|
+
svg.addEventListener("pointermove", this.onPointerMove);
|
|
2615
|
+
svg.addEventListener("pointerout", this.onPointerOut);
|
|
2616
|
+
}
|
|
2617
|
+
position(clientX, clientY) {
|
|
2618
|
+
const OFFSET = 12;
|
|
2619
|
+
const rect = this.el.getBoundingClientRect();
|
|
2620
|
+
const viewportW = window.innerWidth;
|
|
2621
|
+
const viewportH = window.innerHeight;
|
|
2622
|
+
let left = clientX + OFFSET;
|
|
2623
|
+
let top = clientY + OFFSET;
|
|
2624
|
+
if (left + rect.width > viewportW) left = clientX - OFFSET - rect.width;
|
|
2625
|
+
if (top + rect.height > viewportH) top = clientY - OFFSET - rect.height;
|
|
2626
|
+
this.el.style.left = `${Math.max(0, left)}px`;
|
|
2627
|
+
this.el.style.top = `${Math.max(0, top)}px`;
|
|
2628
|
+
}
|
|
2629
|
+
hide() {
|
|
2630
|
+
this.hoveredTaskId = null;
|
|
2631
|
+
this.el.style.display = "none";
|
|
2632
|
+
}
|
|
2633
|
+
destroy() {
|
|
2634
|
+
if (this.svg) {
|
|
2635
|
+
this.svg.removeEventListener("pointerover", this.onPointerOver);
|
|
2636
|
+
this.svg.removeEventListener("pointermove", this.onPointerMove);
|
|
2637
|
+
this.svg.removeEventListener("pointerout", this.onPointerOut);
|
|
2638
|
+
}
|
|
2639
|
+
this.el.remove();
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
|
|
2443
2643
|
// src/index.ts
|
|
2444
2644
|
var HAS_DOM = typeof document !== "undefined" && typeof window !== "undefined";
|
|
2445
2645
|
var DEFAULT_COLUMNS = [{ id: "name", title: "Name" }];
|
|
@@ -2459,6 +2659,9 @@ var _GanttChart = class _GanttChart {
|
|
|
2459
2659
|
this.history = null;
|
|
2460
2660
|
this.renderer = null;
|
|
2461
2661
|
this.interactions = null;
|
|
2662
|
+
this.tooltip = null;
|
|
2663
|
+
this.resizeObserver = null;
|
|
2664
|
+
this.sort = null;
|
|
2462
2665
|
this.renderModel = null;
|
|
2463
2666
|
this.rafHandle = null;
|
|
2464
2667
|
this.currentPage = 1;
|
|
@@ -2481,6 +2684,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2481
2684
|
showCriticalPath: options.showCriticalPath ?? false,
|
|
2482
2685
|
showBaseline: options.showBaseline ?? false,
|
|
2483
2686
|
showDeadlines: options.showDeadlines ?? true,
|
|
2687
|
+
showTooltip: options.showTooltip ?? true,
|
|
2688
|
+
autoFitToViewport: options.autoFitToViewport ?? false,
|
|
2484
2689
|
showAssigneeAvatars: options.showAssigneeAvatars ?? false,
|
|
2485
2690
|
enableHistory: options.enableHistory ?? false,
|
|
2486
2691
|
keyboardAccessible: options.keyboardAccessible ?? false,
|
|
@@ -2501,13 +2706,16 @@ var _GanttChart = class _GanttChart {
|
|
|
2501
2706
|
onDependencyRemove: options.onDependencyRemove,
|
|
2502
2707
|
onDependencyDblClick: options.onDependencyDblClick,
|
|
2503
2708
|
onTaskClick: options.onTaskClick,
|
|
2709
|
+
onTaskDblClick: options.onTaskDblClick,
|
|
2504
2710
|
onGroupToggle: options.onGroupToggle,
|
|
2505
2711
|
onContextMenu: options.onContextMenu,
|
|
2506
2712
|
onTaskCreate: options.onTaskCreate,
|
|
2507
2713
|
onColumnResize: options.onColumnResize,
|
|
2508
2714
|
onColumnReorder: options.onColumnReorder,
|
|
2509
2715
|
onTaskReorder: options.onTaskReorder,
|
|
2510
|
-
onSelectionChange: options.onSelectionChange
|
|
2716
|
+
onSelectionChange: options.onSelectionChange,
|
|
2717
|
+
onSortChange: options.onSortChange,
|
|
2718
|
+
renderTooltip: options.renderTooltip
|
|
2511
2719
|
};
|
|
2512
2720
|
if (this.options.enableHistory) {
|
|
2513
2721
|
this.history = new HistoryManager((state) => this.emitter.emit("history-change", state));
|
|
@@ -2530,7 +2738,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2530
2738
|
onRenameCommit: (taskId, name) => this.updateTask(taskId, { name }),
|
|
2531
2739
|
onColumnResize: this.options.onColumnResize ? (columnId, width) => this.handleColumnResize(columnId, width) : void 0,
|
|
2532
2740
|
onColumnReorder: this.options.onColumnReorder ? (order) => this.handleColumnReorder(order) : void 0,
|
|
2533
|
-
onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0
|
|
2741
|
+
onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
|
|
2742
|
+
onSortClick: (columnId) => this.handleSortClick(columnId)
|
|
2534
2743
|
});
|
|
2535
2744
|
this.interactions = new InteractionController(
|
|
2536
2745
|
this.renderer.svg,
|
|
@@ -2551,7 +2760,17 @@ var _GanttChart = class _GanttChart {
|
|
|
2551
2760
|
onCreateTaskDrag: this.options.onTaskCreate ? (rowTaskId, startXPx, endXPx) => this.handleCreateTaskDrag(rowTaskId, startXPx, endXPx) : void 0,
|
|
2552
2761
|
onLinkDblClick: this.options.onDependencyDblClick ? (fromId, toId) => {
|
|
2553
2762
|
const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
|
|
2554
|
-
if (dep)
|
|
2763
|
+
if (dep) {
|
|
2764
|
+
this.emitter.emit("dependency-dblclick", dep);
|
|
2765
|
+
this.options.onDependencyDblClick?.(dep);
|
|
2766
|
+
}
|
|
2767
|
+
} : void 0,
|
|
2768
|
+
onBarDblClick: this.options.onTaskDblClick ? (taskId) => {
|
|
2769
|
+
const task = this.tasks.find((t) => t.id === taskId);
|
|
2770
|
+
if (task) {
|
|
2771
|
+
this.emitter.emit("task-dblclick", { task });
|
|
2772
|
+
this.options.onTaskDblClick?.(task);
|
|
2773
|
+
}
|
|
2555
2774
|
} : void 0,
|
|
2556
2775
|
onContextMenu: this.options.onContextMenu ? (taskId, evt) => {
|
|
2557
2776
|
const task = this.tasks.find((t) => t.id === taskId);
|
|
@@ -2570,6 +2789,17 @@ var _GanttChart = class _GanttChart {
|
|
|
2570
2789
|
pxPerMs: () => pxPerMs(this.options.viewMode, this.columnWidth)
|
|
2571
2790
|
}
|
|
2572
2791
|
);
|
|
2792
|
+
this.tooltip = new Tooltip(
|
|
2793
|
+
() => ({ enabled: this.options.showTooltip, render: this.options.renderTooltip }),
|
|
2794
|
+
(taskId) => this.tasks.find((t) => t.id === taskId)
|
|
2795
|
+
);
|
|
2796
|
+
this.tooltip.attach(this.renderer.svg);
|
|
2797
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
2798
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
2799
|
+
if (this.options.autoFitToViewport) this.scheduleRender();
|
|
2800
|
+
});
|
|
2801
|
+
this.resizeObserver.observe(this.container);
|
|
2802
|
+
}
|
|
2573
2803
|
}
|
|
2574
2804
|
handleMove(taskId, dxMs) {
|
|
2575
2805
|
if (dxMs === 0) return;
|
|
@@ -2974,6 +3204,24 @@ var _GanttChart = class _GanttChart {
|
|
|
2974
3204
|
this.options.onColumnReorder?.(order);
|
|
2975
3205
|
this.scheduleRender();
|
|
2976
3206
|
}
|
|
3207
|
+
/** Cycles a sortable column's header through asc -> desc -> none; clicking a different column starts it fresh at asc. */
|
|
3208
|
+
handleSortClick(columnId) {
|
|
3209
|
+
if (this.sort?.columnId === columnId) {
|
|
3210
|
+
this.setSort(columnId, this.sort.direction === "asc" ? "desc" : null);
|
|
3211
|
+
} else {
|
|
3212
|
+
this.setSort(columnId, "asc");
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
/** Sorts siblings at every tree level by a column's value (see GanttColumn.sortable); `direction: null`/omitted `columnId` clears it. */
|
|
3216
|
+
setSort(columnId, direction = "asc") {
|
|
3217
|
+
this.sort = columnId && direction ? { columnId, direction } : null;
|
|
3218
|
+
this.emitter.emit("sort-change", { sort: this.sort });
|
|
3219
|
+
this.options.onSortChange?.(this.sort);
|
|
3220
|
+
this.scheduleRender();
|
|
3221
|
+
}
|
|
3222
|
+
getSort() {
|
|
3223
|
+
return this.sort;
|
|
3224
|
+
}
|
|
2977
3225
|
handleRowReorder(draggedId, targetId, position) {
|
|
2978
3226
|
const draggedIndex = this.tasks.findIndex((t) => t.id === draggedId);
|
|
2979
3227
|
const targetIndex = this.tasks.findIndex((t) => t.id === targetId);
|
|
@@ -3040,13 +3288,18 @@ var _GanttChart = class _GanttChart {
|
|
|
3040
3288
|
this.scheduleRender();
|
|
3041
3289
|
}
|
|
3042
3290
|
setOptions(partial) {
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
if (
|
|
3291
|
+
const hasTheme = "theme" in partial;
|
|
3292
|
+
const hasColorScheme = "colorScheme" in partial;
|
|
3293
|
+
if (hasColorScheme) this.colorScheme = partial.colorScheme ?? "auto";
|
|
3294
|
+
if (hasTheme) this.explicitTheme = partial.theme;
|
|
3295
|
+
if (hasTheme || hasColorScheme) {
|
|
3046
3296
|
this.theme = mergeTheme(this.explicitTheme, this.colorScheme);
|
|
3047
3297
|
}
|
|
3048
|
-
if (partial
|
|
3298
|
+
if ("columns" in partial) this.columns = partial.columns ?? this.columns;
|
|
3049
3299
|
if (partial.columnWidth !== void 0) this.columnWidth = partial.columnWidth;
|
|
3300
|
+
if (partial.gridPanelWidth !== void 0) {
|
|
3301
|
+
this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
|
|
3302
|
+
}
|
|
3050
3303
|
Object.assign(this.options, partial);
|
|
3051
3304
|
this.scheduleRender();
|
|
3052
3305
|
}
|
|
@@ -3149,7 +3402,8 @@ var _GanttChart = class _GanttChart {
|
|
|
3149
3402
|
markers: this.options.markers,
|
|
3150
3403
|
autoRollupProgress: this.options.autoRollupProgress,
|
|
3151
3404
|
selectedTaskIds: this.options.selectable ? this.selectedTaskIds : void 0,
|
|
3152
|
-
pagination
|
|
3405
|
+
pagination,
|
|
3406
|
+
sort: this.sort
|
|
3153
3407
|
});
|
|
3154
3408
|
}
|
|
3155
3409
|
scheduleRender(immediate = false) {
|
|
@@ -3164,6 +3418,7 @@ var _GanttChart = class _GanttChart {
|
|
|
3164
3418
|
});
|
|
3165
3419
|
}
|
|
3166
3420
|
doRender() {
|
|
3421
|
+
if (this.options.autoFitToViewport) this.applyAutoFit();
|
|
3167
3422
|
this.renderModel = this.computeModel();
|
|
3168
3423
|
this.renderer?.render(this.renderModel, this.tasks, this.columns, this.explicitTheme);
|
|
3169
3424
|
if (this.renderer && this.colorScheme !== "auto") {
|
|
@@ -3172,6 +3427,29 @@ var _GanttChart = class _GanttChart {
|
|
|
3172
3427
|
this.renderer?.root.removeAttribute("data-gantt-theme");
|
|
3173
3428
|
}
|
|
3174
3429
|
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Stretch-only fit: bump columnWidth up so the timeline fills the container's available
|
|
3432
|
+
* width, but never shrink it below what the current zoom level already implies - a project
|
|
3433
|
+
* wider than the container should still scroll normally, not get crammed to fit.
|
|
3434
|
+
*/
|
|
3435
|
+
applyAutoFit() {
|
|
3436
|
+
const availableWidth = this.container.clientWidth - this.options.gridPanelWidth;
|
|
3437
|
+
if (!Number.isFinite(availableWidth) || availableWidth <= 0) return;
|
|
3438
|
+
let min = null;
|
|
3439
|
+
let max = null;
|
|
3440
|
+
for (const t of this.tasks) {
|
|
3441
|
+
if (!isValidDate(t.start) || !isValidDate(t.end)) continue;
|
|
3442
|
+
min = min === null ? t.start.getTime() : Math.min(min, t.start.getTime());
|
|
3443
|
+
max = max === null ? t.end.getTime() : Math.max(max, t.end.getTime());
|
|
3444
|
+
}
|
|
3445
|
+
if (min === null || max === null || max <= min) return;
|
|
3446
|
+
const rangeMs = max - min;
|
|
3447
|
+
const unitMs = approxUnitMs(this.options.viewMode);
|
|
3448
|
+
const neededColumnWidth = availableWidth * unitMs / rangeMs;
|
|
3449
|
+
if (neededColumnWidth > this.columnWidth) {
|
|
3450
|
+
this.columnWidth = Math.min(neededColumnWidth, _GanttChart.MAX_COLUMN_WIDTH);
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3175
3453
|
undo() {
|
|
3176
3454
|
this.history?.undo();
|
|
3177
3455
|
}
|
|
@@ -3266,6 +3544,8 @@ var _GanttChart = class _GanttChart {
|
|
|
3266
3544
|
this.darkMediaQuery?.removeEventListener?.("change", this.handleSchemeChange);
|
|
3267
3545
|
this.emitter.removeAllListeners();
|
|
3268
3546
|
this.interactions?.destroy();
|
|
3547
|
+
this.tooltip?.destroy();
|
|
3548
|
+
this.resizeObserver?.disconnect();
|
|
3269
3549
|
this.renderer?.destroy();
|
|
3270
3550
|
}
|
|
3271
3551
|
};
|