@ganttloom/gantt-core 0.2.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/README.md +147 -5
- package/dist/index.cjs +364 -44
- 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 +364 -44
- package/dist/index.js.map +1 -1
- package/dist/styles.css +51 -0
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -189,7 +189,6 @@ function computeCriticalPath(tasks, dependencies) {
|
|
|
189
189
|
}
|
|
190
190
|
for (const e of edges) {
|
|
191
191
|
outgoing.get(e.fromId)?.push(e);
|
|
192
|
-
incoming.get(e.fromId) === void 0 ? void 0 : void 0;
|
|
193
192
|
incoming.get(e.toId)?.push(e);
|
|
194
193
|
}
|
|
195
194
|
const inDegree = /* @__PURE__ */ new Map();
|
|
@@ -274,11 +273,9 @@ function computeCriticalPath(tasks, dependencies) {
|
|
|
274
273
|
criticalLinks.add(linkKey(e.fromId, e.toId));
|
|
275
274
|
}
|
|
276
275
|
}
|
|
277
|
-
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
278
276
|
for (const task of tasks) {
|
|
279
277
|
if (!task.isGroup) continue;
|
|
280
278
|
let hasCriticalDescendant = false;
|
|
281
|
-
let cur = task;
|
|
282
279
|
const descendantStack = [task.id];
|
|
283
280
|
const visited = /* @__PURE__ */ new Set();
|
|
284
281
|
while (descendantStack.length > 0) {
|
|
@@ -293,8 +290,6 @@ function computeCriticalPath(tasks, dependencies) {
|
|
|
293
290
|
}
|
|
294
291
|
}
|
|
295
292
|
if (hasCriticalDescendant) criticalTasks.add(task.id);
|
|
296
|
-
void cur;
|
|
297
|
-
void taskMap;
|
|
298
293
|
}
|
|
299
294
|
return { criticalTasks, criticalLinks };
|
|
300
295
|
}
|
|
@@ -668,6 +663,29 @@ function computeProgressRollup(node) {
|
|
|
668
663
|
}
|
|
669
664
|
node.computedProgress = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
670
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
|
+
}
|
|
671
689
|
function flatten(roots) {
|
|
672
690
|
const out = [];
|
|
673
691
|
const stack = [];
|
|
@@ -689,6 +707,9 @@ function flatten(roots) {
|
|
|
689
707
|
}
|
|
690
708
|
return out;
|
|
691
709
|
}
|
|
710
|
+
function estimateTextWidth(text, fontSize) {
|
|
711
|
+
return text.length * fontSize * 0.62;
|
|
712
|
+
}
|
|
692
713
|
function anchorPoint(bar, side) {
|
|
693
714
|
const y = bar.y + bar.height / 2;
|
|
694
715
|
return side === "start" ? { x: bar.x, y } : { x: bar.x + bar.width, y };
|
|
@@ -751,6 +772,14 @@ function linkPath(from, to) {
|
|
|
751
772
|
const c2y = to.y;
|
|
752
773
|
return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
|
|
753
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
|
+
};
|
|
754
783
|
function computeLayout(input) {
|
|
755
784
|
const {
|
|
756
785
|
tasks,
|
|
@@ -766,7 +795,8 @@ function computeLayout(input) {
|
|
|
766
795
|
markers = [],
|
|
767
796
|
autoRollupProgress = false,
|
|
768
797
|
selectedTaskIds,
|
|
769
|
-
pagination
|
|
798
|
+
pagination,
|
|
799
|
+
sort = null
|
|
770
800
|
} = input;
|
|
771
801
|
validateInputs(tasks, dependencies);
|
|
772
802
|
const { roots, nodeById } = buildTree(tasks);
|
|
@@ -774,6 +804,7 @@ function computeLayout(input) {
|
|
|
774
804
|
if (autoRollupProgress) {
|
|
775
805
|
for (const root of roots) computeProgressRollup(root);
|
|
776
806
|
}
|
|
807
|
+
sortTree(roots, columns, sort);
|
|
777
808
|
let visibleRoots = roots;
|
|
778
809
|
if (pagination && pagination.pageSize > 0) {
|
|
779
810
|
const start = Math.max(0, (pagination.page - 1) * pagination.pageSize);
|
|
@@ -793,8 +824,15 @@ function computeLayout(input) {
|
|
|
793
824
|
const now = Date.now();
|
|
794
825
|
const rawRangeStart = rangeMin === null ? new Date(now) : new Date(rangeMin);
|
|
795
826
|
const rawRangeEnd = rangeMax === null ? new Date(now + 1) : new Date(rangeMax);
|
|
796
|
-
|
|
797
|
-
|
|
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
|
+
}
|
|
798
836
|
const rows = [];
|
|
799
837
|
const bars = [];
|
|
800
838
|
const barByTaskId = /* @__PURE__ */ new Map();
|
|
@@ -814,8 +852,9 @@ function computeLayout(input) {
|
|
|
814
852
|
const end = row.node.computedEnd;
|
|
815
853
|
if (!start || !end) return;
|
|
816
854
|
const x = dateToX(start, paddedStart, viewMode, columnWidth);
|
|
817
|
-
const
|
|
818
|
-
if (!Number.isFinite(x) || !Number.isFinite(
|
|
855
|
+
const rawWidth = Math.max(0, dateToX(end, paddedStart, viewMode, columnWidth) - x);
|
|
856
|
+
if (!Number.isFinite(x) || !Number.isFinite(rawWidth)) return;
|
|
857
|
+
const width2 = rawWidth > 0 ? Math.max(rawWidth, 3) : rawWidth;
|
|
819
858
|
const progress = task.progress !== void 0 ? Math.min(100, Math.max(0, task.progress)) : autoRollupProgress ? row.node.computedProgress : 0;
|
|
820
859
|
const barHeight = theme.barHeight;
|
|
821
860
|
const barY = y + (theme.rowHeight - barHeight) / 2;
|
|
@@ -923,7 +962,13 @@ function computeLayout(input) {
|
|
|
923
962
|
})).filter((m) => Number.isFinite(m.x));
|
|
924
963
|
let width = 0;
|
|
925
964
|
for (const tick of ticks) width = Math.max(width, tick.x);
|
|
926
|
-
for (const bar of bars)
|
|
965
|
+
for (const bar of bars) {
|
|
966
|
+
width = Math.max(width, bar.x + bar.width);
|
|
967
|
+
if (bar.label) {
|
|
968
|
+
const labelOffset = bar.isMilestone ? theme.barHeight * 0.4 + 8 : bar.width + 6;
|
|
969
|
+
width = Math.max(width, bar.x + labelOffset + estimateTextWidth(bar.label, theme.fontSize));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
927
972
|
width += columnWidth;
|
|
928
973
|
const height = rows.length * theme.rowHeight;
|
|
929
974
|
return {
|
|
@@ -938,7 +983,8 @@ function computeLayout(input) {
|
|
|
938
983
|
columns,
|
|
939
984
|
theme,
|
|
940
985
|
markers: renderMarkers,
|
|
941
|
-
rangeStart: paddedStart
|
|
986
|
+
rangeStart: paddedStart,
|
|
987
|
+
sort
|
|
942
988
|
};
|
|
943
989
|
}
|
|
944
990
|
|
|
@@ -1135,6 +1181,7 @@ var GanttRenderer = class {
|
|
|
1135
1181
|
this.lastModel = null;
|
|
1136
1182
|
this.lastTasks = [];
|
|
1137
1183
|
this.lastColumns = [];
|
|
1184
|
+
this.lastExplicitThemeKeys = /* @__PURE__ */ new Set();
|
|
1138
1185
|
this.onGridDoubleClick = (evt) => {
|
|
1139
1186
|
if (!this.options.onRenameCommit) return;
|
|
1140
1187
|
const target = evt.target;
|
|
@@ -1172,8 +1219,13 @@ var GanttRenderer = class {
|
|
|
1172
1219
|
this.root = el("div", "gantt-root");
|
|
1173
1220
|
this.gridPanel = el("div", "gantt-grid-panel");
|
|
1174
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);
|
|
1175
1226
|
this.svg = svgEl("svg");
|
|
1176
1227
|
this.svg.classList.add("gantt-svg");
|
|
1228
|
+
this.timelineScroll.appendChild(this.headerContainer);
|
|
1177
1229
|
this.timelineScroll.appendChild(this.svg);
|
|
1178
1230
|
this.root.appendChild(this.gridPanel);
|
|
1179
1231
|
this.root.appendChild(this.timelineScroll);
|
|
@@ -1189,9 +1241,14 @@ var GanttRenderer = class {
|
|
|
1189
1241
|
this.lastTasks = tasks;
|
|
1190
1242
|
this.lastColumns = columns;
|
|
1191
1243
|
const cssVars = explicitThemeToCssVars(explicitTheme);
|
|
1192
|
-
|
|
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) {
|
|
1193
1249
|
this.root.style.setProperty(key, cssVars[key]);
|
|
1194
1250
|
}
|
|
1251
|
+
this.lastExplicitThemeKeys = newKeys;
|
|
1195
1252
|
this.renderGridPanel(model, tasks, columns);
|
|
1196
1253
|
this.renderTimeline(model, tasks);
|
|
1197
1254
|
}
|
|
@@ -1219,7 +1276,7 @@ var GanttRenderer = class {
|
|
|
1219
1276
|
const header = el("div", "gantt-grid-header-row");
|
|
1220
1277
|
header.style.height = `${model.headerHeight}px`;
|
|
1221
1278
|
for (const col of columns) {
|
|
1222
|
-
header.appendChild(this.renderHeaderCell(col, columns));
|
|
1279
|
+
header.appendChild(this.renderHeaderCell(col, columns, model.sort));
|
|
1223
1280
|
}
|
|
1224
1281
|
this.gridPanel.appendChild(header);
|
|
1225
1282
|
const body = el("div", "gantt-grid-body");
|
|
@@ -1232,11 +1289,23 @@ var GanttRenderer = class {
|
|
|
1232
1289
|
});
|
|
1233
1290
|
this.gridPanel.appendChild(body);
|
|
1234
1291
|
}
|
|
1235
|
-
renderHeaderCell(col, columns) {
|
|
1292
|
+
renderHeaderCell(col, columns, sort) {
|
|
1236
1293
|
const cell = el("div", "gantt-grid-header-cell");
|
|
1237
1294
|
cell.style.position = "relative";
|
|
1238
1295
|
if (col.width) cell.style.width = `${col.width}px`;
|
|
1239
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
|
+
}
|
|
1240
1309
|
if (this.options.onColumnReorder) {
|
|
1241
1310
|
cell.draggable = true;
|
|
1242
1311
|
cell.style.cursor = "grab";
|
|
@@ -1363,8 +1432,8 @@ var GanttRenderer = class {
|
|
|
1363
1432
|
renderTimeline(model, tasks) {
|
|
1364
1433
|
this.svg.replaceChildren();
|
|
1365
1434
|
this.svg.setAttribute("width", String(model.width));
|
|
1366
|
-
this.svg.setAttribute("height", String(model.
|
|
1367
|
-
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}`);
|
|
1368
1437
|
const defs = svgEl("defs");
|
|
1369
1438
|
const marker = svgEl("marker");
|
|
1370
1439
|
marker.setAttribute("id", this.markerDefsId);
|
|
@@ -1381,7 +1450,6 @@ var GanttRenderer = class {
|
|
|
1381
1450
|
this.svg.appendChild(defs);
|
|
1382
1451
|
const { startY, endY } = this.getViewportRowRange(model);
|
|
1383
1452
|
const bodyGroup = svgEl("g");
|
|
1384
|
-
bodyGroup.setAttribute("transform", `translate(0, ${model.headerHeight})`);
|
|
1385
1453
|
this.renderGridLines(bodyGroup, model);
|
|
1386
1454
|
this.renderRowBackgrounds(bodyGroup, model, startY, endY);
|
|
1387
1455
|
this.renderBars(bodyGroup, model, tasks, startY, endY);
|
|
@@ -1422,7 +1490,9 @@ var GanttRenderer = class {
|
|
|
1422
1490
|
}
|
|
1423
1491
|
}
|
|
1424
1492
|
renderMarkers(group, model) {
|
|
1425
|
-
|
|
1493
|
+
const sorted = [...model.markers].sort((a, b) => a.x - b.x);
|
|
1494
|
+
let defs = null;
|
|
1495
|
+
sorted.forEach((marker, i) => {
|
|
1426
1496
|
const line = svgEl("line");
|
|
1427
1497
|
line.setAttribute("x1", String(marker.x));
|
|
1428
1498
|
line.setAttribute("x2", String(marker.x));
|
|
@@ -1434,21 +1504,41 @@ var GanttRenderer = class {
|
|
|
1434
1504
|
line.dataset.ganttMarker = marker.label;
|
|
1435
1505
|
group.appendChild(line);
|
|
1436
1506
|
if (marker.label) {
|
|
1507
|
+
const nextX = sorted[i + 1]?.x ?? model.width;
|
|
1508
|
+
const cellWidth = Math.max(0, nextX - marker.x);
|
|
1509
|
+
if (!defs) {
|
|
1510
|
+
defs = svgEl("defs");
|
|
1511
|
+
group.insertBefore(defs, group.firstChild);
|
|
1512
|
+
}
|
|
1513
|
+
const clipId = `${this.markerDefsId}-marker-${i}`;
|
|
1514
|
+
const clipPath = svgEl("clipPath");
|
|
1515
|
+
clipPath.setAttribute("id", clipId);
|
|
1516
|
+
const clipRect = svgEl("rect");
|
|
1517
|
+
clipRect.setAttribute("x", String(marker.x));
|
|
1518
|
+
clipRect.setAttribute("y", "0");
|
|
1519
|
+
clipRect.setAttribute("width", String(cellWidth));
|
|
1520
|
+
clipRect.setAttribute("height", String(model.height));
|
|
1521
|
+
clipPath.appendChild(clipRect);
|
|
1522
|
+
defs.appendChild(clipPath);
|
|
1437
1523
|
const label = svgEl("text");
|
|
1438
1524
|
label.setAttribute("x", String(marker.x + 4));
|
|
1439
1525
|
label.setAttribute("y", "12");
|
|
1526
|
+
label.setAttribute("clip-path", `url(#${clipId})`);
|
|
1440
1527
|
label.setAttribute("fill", marker.color);
|
|
1441
1528
|
label.setAttribute("font-size", String(model.theme.fontSize));
|
|
1442
1529
|
label.setAttribute("font-family", model.theme.fontFamily);
|
|
1443
1530
|
label.textContent = marker.label;
|
|
1444
1531
|
group.appendChild(label);
|
|
1445
1532
|
}
|
|
1446
|
-
}
|
|
1533
|
+
});
|
|
1447
1534
|
}
|
|
1448
1535
|
renderHeader(model) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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");
|
|
1452
1542
|
headerGroup.setAttribute("class", "gantt-header-group");
|
|
1453
1543
|
const bg = svgEl("rect");
|
|
1454
1544
|
bg.setAttribute("x", "0");
|
|
@@ -1457,17 +1547,33 @@ var GanttRenderer = class {
|
|
|
1457
1547
|
bg.setAttribute("height", String(model.headerHeight));
|
|
1458
1548
|
bg.style.fill = themedFill("backgroundColor", model.theme.backgroundColor);
|
|
1459
1549
|
headerGroup.appendChild(bg);
|
|
1460
|
-
|
|
1550
|
+
const defs = svgEl("defs");
|
|
1551
|
+
const ticks = model.ticks;
|
|
1552
|
+
for (let i = 0; i < ticks.length; i++) {
|
|
1553
|
+
const tick = ticks[i];
|
|
1554
|
+
const cellWidth = (ticks[i + 1]?.x ?? model.width) - tick.x;
|
|
1555
|
+
const clipId = `${this.markerDefsId}-tick-${i}`;
|
|
1556
|
+
const clipPath = svgEl("clipPath");
|
|
1557
|
+
clipPath.setAttribute("id", clipId);
|
|
1558
|
+
const clipRect = svgEl("rect");
|
|
1559
|
+
clipRect.setAttribute("x", String(tick.x));
|
|
1560
|
+
clipRect.setAttribute("y", "0");
|
|
1561
|
+
clipRect.setAttribute("width", String(Math.max(0, cellWidth)));
|
|
1562
|
+
clipRect.setAttribute("height", String(model.headerHeight));
|
|
1563
|
+
clipPath.appendChild(clipRect);
|
|
1564
|
+
defs.appendChild(clipPath);
|
|
1461
1565
|
const text = svgEl("text");
|
|
1462
1566
|
text.setAttribute("x", String(tick.x + 4));
|
|
1463
1567
|
text.setAttribute("y", String(model.headerHeight - 8));
|
|
1568
|
+
text.setAttribute("clip-path", `url(#${clipId})`);
|
|
1464
1569
|
text.style.fill = themedFill("textColor", model.theme.textColor);
|
|
1465
1570
|
text.setAttribute("font-size", String(model.theme.fontSize));
|
|
1466
1571
|
text.setAttribute("font-family", model.theme.fontFamily);
|
|
1467
1572
|
text.textContent = tick.label;
|
|
1468
1573
|
headerGroup.appendChild(text);
|
|
1469
1574
|
}
|
|
1470
|
-
|
|
1575
|
+
headerGroup.insertBefore(defs, headerGroup.firstChild);
|
|
1576
|
+
this.headerSvg.appendChild(headerGroup);
|
|
1471
1577
|
}
|
|
1472
1578
|
/**
|
|
1473
1579
|
* Invisible full-width hit-test rects, one per row, behind the bars. Lets
|
|
@@ -1502,11 +1608,6 @@ var GanttRenderer = class {
|
|
|
1502
1608
|
g.setAttribute("class", "gantt-bar");
|
|
1503
1609
|
g.dataset.ganttBar = bar.taskId;
|
|
1504
1610
|
g.setAttribute("transform", `translate(${bar.x}, ${bar.y})`);
|
|
1505
|
-
if (task?.notes) {
|
|
1506
|
-
const title = svgEl("title");
|
|
1507
|
-
title.textContent = task.notes;
|
|
1508
|
-
g.appendChild(title);
|
|
1509
|
-
}
|
|
1510
1611
|
if (this.options.keyboardAccessible) {
|
|
1511
1612
|
g.setAttribute("tabindex", "0");
|
|
1512
1613
|
g.setAttribute("role", "button");
|
|
@@ -1751,8 +1852,30 @@ var GanttRenderer = class {
|
|
|
1751
1852
|
group.appendChild(path);
|
|
1752
1853
|
}
|
|
1753
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
|
+
*/
|
|
1754
1860
|
toSVGString() {
|
|
1755
|
-
|
|
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);
|
|
1756
1879
|
}
|
|
1757
1880
|
destroy() {
|
|
1758
1881
|
if (this.scrollListener) {
|
|
@@ -2131,13 +2254,17 @@ var InteractionController = class {
|
|
|
2131
2254
|
}
|
|
2132
2255
|
};
|
|
2133
2256
|
this.onSvgDoubleClick = (evt) => {
|
|
2134
|
-
if (!this.callbacks.onLinkDblClick) return;
|
|
2135
2257
|
const target = evt.target;
|
|
2136
2258
|
const linkPath2 = target.closest("[data-gantt-link]");
|
|
2137
2259
|
const key = linkPath2?.dataset.ganttLink;
|
|
2138
|
-
if (
|
|
2139
|
-
|
|
2140
|
-
|
|
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);
|
|
2141
2268
|
};
|
|
2142
2269
|
this.onPointerDown = (evt) => {
|
|
2143
2270
|
const target = evt.target;
|
|
@@ -2400,6 +2527,119 @@ var InteractionController = class {
|
|
|
2400
2527
|
}
|
|
2401
2528
|
};
|
|
2402
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
|
+
|
|
2403
2643
|
// src/index.ts
|
|
2404
2644
|
var HAS_DOM = typeof document !== "undefined" && typeof window !== "undefined";
|
|
2405
2645
|
var DEFAULT_COLUMNS = [{ id: "name", title: "Name" }];
|
|
@@ -2419,6 +2659,9 @@ var _GanttChart = class _GanttChart {
|
|
|
2419
2659
|
this.history = null;
|
|
2420
2660
|
this.renderer = null;
|
|
2421
2661
|
this.interactions = null;
|
|
2662
|
+
this.tooltip = null;
|
|
2663
|
+
this.resizeObserver = null;
|
|
2664
|
+
this.sort = null;
|
|
2422
2665
|
this.renderModel = null;
|
|
2423
2666
|
this.rafHandle = null;
|
|
2424
2667
|
this.currentPage = 1;
|
|
@@ -2441,6 +2684,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2441
2684
|
showCriticalPath: options.showCriticalPath ?? false,
|
|
2442
2685
|
showBaseline: options.showBaseline ?? false,
|
|
2443
2686
|
showDeadlines: options.showDeadlines ?? true,
|
|
2687
|
+
showTooltip: options.showTooltip ?? true,
|
|
2688
|
+
autoFitToViewport: options.autoFitToViewport ?? false,
|
|
2444
2689
|
showAssigneeAvatars: options.showAssigneeAvatars ?? false,
|
|
2445
2690
|
enableHistory: options.enableHistory ?? false,
|
|
2446
2691
|
keyboardAccessible: options.keyboardAccessible ?? false,
|
|
@@ -2461,13 +2706,16 @@ var _GanttChart = class _GanttChart {
|
|
|
2461
2706
|
onDependencyRemove: options.onDependencyRemove,
|
|
2462
2707
|
onDependencyDblClick: options.onDependencyDblClick,
|
|
2463
2708
|
onTaskClick: options.onTaskClick,
|
|
2709
|
+
onTaskDblClick: options.onTaskDblClick,
|
|
2464
2710
|
onGroupToggle: options.onGroupToggle,
|
|
2465
2711
|
onContextMenu: options.onContextMenu,
|
|
2466
2712
|
onTaskCreate: options.onTaskCreate,
|
|
2467
2713
|
onColumnResize: options.onColumnResize,
|
|
2468
2714
|
onColumnReorder: options.onColumnReorder,
|
|
2469
2715
|
onTaskReorder: options.onTaskReorder,
|
|
2470
|
-
onSelectionChange: options.onSelectionChange
|
|
2716
|
+
onSelectionChange: options.onSelectionChange,
|
|
2717
|
+
onSortChange: options.onSortChange,
|
|
2718
|
+
renderTooltip: options.renderTooltip
|
|
2471
2719
|
};
|
|
2472
2720
|
if (this.options.enableHistory) {
|
|
2473
2721
|
this.history = new HistoryManager((state) => this.emitter.emit("history-change", state));
|
|
@@ -2490,7 +2738,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2490
2738
|
onRenameCommit: (taskId, name) => this.updateTask(taskId, { name }),
|
|
2491
2739
|
onColumnResize: this.options.onColumnResize ? (columnId, width) => this.handleColumnResize(columnId, width) : void 0,
|
|
2492
2740
|
onColumnReorder: this.options.onColumnReorder ? (order) => this.handleColumnReorder(order) : void 0,
|
|
2493
|
-
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)
|
|
2494
2743
|
});
|
|
2495
2744
|
this.interactions = new InteractionController(
|
|
2496
2745
|
this.renderer.svg,
|
|
@@ -2511,7 +2760,17 @@ var _GanttChart = class _GanttChart {
|
|
|
2511
2760
|
onCreateTaskDrag: this.options.onTaskCreate ? (rowTaskId, startXPx, endXPx) => this.handleCreateTaskDrag(rowTaskId, startXPx, endXPx) : void 0,
|
|
2512
2761
|
onLinkDblClick: this.options.onDependencyDblClick ? (fromId, toId) => {
|
|
2513
2762
|
const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
|
|
2514
|
-
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
|
+
}
|
|
2515
2774
|
} : void 0,
|
|
2516
2775
|
onContextMenu: this.options.onContextMenu ? (taskId, evt) => {
|
|
2517
2776
|
const task = this.tasks.find((t) => t.id === taskId);
|
|
@@ -2530,6 +2789,17 @@ var _GanttChart = class _GanttChart {
|
|
|
2530
2789
|
pxPerMs: () => pxPerMs(this.options.viewMode, this.columnWidth)
|
|
2531
2790
|
}
|
|
2532
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
|
+
}
|
|
2533
2803
|
}
|
|
2534
2804
|
handleMove(taskId, dxMs) {
|
|
2535
2805
|
if (dxMs === 0) return;
|
|
@@ -2550,7 +2820,7 @@ var _GanttChart = class _GanttChart {
|
|
|
2550
2820
|
const newStart = edge === "left" ? new Date(task.start.getTime() + dxMs) : task.start;
|
|
2551
2821
|
const newEnd = edge === "right" ? new Date(task.end.getTime() + dxMs) : task.end;
|
|
2552
2822
|
if (newEnd.getTime() <= newStart.getTime()) return;
|
|
2553
|
-
|
|
2823
|
+
const newSegments = task.segments?.map((s) => ({ ...s }));
|
|
2554
2824
|
if (newSegments && newSegments.length > 0) {
|
|
2555
2825
|
if (edge === "left") newSegments[0] = { ...newSegments[0], start: newStart };
|
|
2556
2826
|
else newSegments[newSegments.length - 1] = { ...newSegments[newSegments.length - 1], end: newEnd };
|
|
@@ -2934,6 +3204,24 @@ var _GanttChart = class _GanttChart {
|
|
|
2934
3204
|
this.options.onColumnReorder?.(order);
|
|
2935
3205
|
this.scheduleRender();
|
|
2936
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
|
+
}
|
|
2937
3225
|
handleRowReorder(draggedId, targetId, position) {
|
|
2938
3226
|
const draggedIndex = this.tasks.findIndex((t) => t.id === draggedId);
|
|
2939
3227
|
const targetIndex = this.tasks.findIndex((t) => t.id === targetId);
|
|
@@ -3000,13 +3288,18 @@ var _GanttChart = class _GanttChart {
|
|
|
3000
3288
|
this.scheduleRender();
|
|
3001
3289
|
}
|
|
3002
3290
|
setOptions(partial) {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
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) {
|
|
3006
3296
|
this.theme = mergeTheme(this.explicitTheme, this.colorScheme);
|
|
3007
3297
|
}
|
|
3008
|
-
if (partial
|
|
3298
|
+
if ("columns" in partial) this.columns = partial.columns ?? this.columns;
|
|
3009
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
|
+
}
|
|
3010
3303
|
Object.assign(this.options, partial);
|
|
3011
3304
|
this.scheduleRender();
|
|
3012
3305
|
}
|
|
@@ -3109,7 +3402,8 @@ var _GanttChart = class _GanttChart {
|
|
|
3109
3402
|
markers: this.options.markers,
|
|
3110
3403
|
autoRollupProgress: this.options.autoRollupProgress,
|
|
3111
3404
|
selectedTaskIds: this.options.selectable ? this.selectedTaskIds : void 0,
|
|
3112
|
-
pagination
|
|
3405
|
+
pagination,
|
|
3406
|
+
sort: this.sort
|
|
3113
3407
|
});
|
|
3114
3408
|
}
|
|
3115
3409
|
scheduleRender(immediate = false) {
|
|
@@ -3124,6 +3418,7 @@ var _GanttChart = class _GanttChart {
|
|
|
3124
3418
|
});
|
|
3125
3419
|
}
|
|
3126
3420
|
doRender() {
|
|
3421
|
+
if (this.options.autoFitToViewport) this.applyAutoFit();
|
|
3127
3422
|
this.renderModel = this.computeModel();
|
|
3128
3423
|
this.renderer?.render(this.renderModel, this.tasks, this.columns, this.explicitTheme);
|
|
3129
3424
|
if (this.renderer && this.colorScheme !== "auto") {
|
|
@@ -3132,6 +3427,29 @@ var _GanttChart = class _GanttChart {
|
|
|
3132
3427
|
this.renderer?.root.removeAttribute("data-gantt-theme");
|
|
3133
3428
|
}
|
|
3134
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
|
+
}
|
|
3135
3453
|
undo() {
|
|
3136
3454
|
this.history?.undo();
|
|
3137
3455
|
}
|
|
@@ -3226,6 +3544,8 @@ var _GanttChart = class _GanttChart {
|
|
|
3226
3544
|
this.darkMediaQuery?.removeEventListener?.("change", this.handleSchemeChange);
|
|
3227
3545
|
this.emitter.removeAllListeners();
|
|
3228
3546
|
this.interactions?.destroy();
|
|
3547
|
+
this.tooltip?.destroy();
|
|
3548
|
+
this.resizeObserver?.disconnect();
|
|
3229
3549
|
this.renderer?.destroy();
|
|
3230
3550
|
}
|
|
3231
3551
|
};
|