@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.js
CHANGED
|
@@ -140,7 +140,6 @@ function computeCriticalPath(tasks, dependencies) {
|
|
|
140
140
|
}
|
|
141
141
|
for (const e of edges) {
|
|
142
142
|
outgoing.get(e.fromId)?.push(e);
|
|
143
|
-
incoming.get(e.fromId) === void 0 ? void 0 : void 0;
|
|
144
143
|
incoming.get(e.toId)?.push(e);
|
|
145
144
|
}
|
|
146
145
|
const inDegree = /* @__PURE__ */ new Map();
|
|
@@ -225,11 +224,9 @@ function computeCriticalPath(tasks, dependencies) {
|
|
|
225
224
|
criticalLinks.add(linkKey(e.fromId, e.toId));
|
|
226
225
|
}
|
|
227
226
|
}
|
|
228
|
-
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
229
227
|
for (const task of tasks) {
|
|
230
228
|
if (!task.isGroup) continue;
|
|
231
229
|
let hasCriticalDescendant = false;
|
|
232
|
-
let cur = task;
|
|
233
230
|
const descendantStack = [task.id];
|
|
234
231
|
const visited = /* @__PURE__ */ new Set();
|
|
235
232
|
while (descendantStack.length > 0) {
|
|
@@ -244,8 +241,6 @@ function computeCriticalPath(tasks, dependencies) {
|
|
|
244
241
|
}
|
|
245
242
|
}
|
|
246
243
|
if (hasCriticalDescendant) criticalTasks.add(task.id);
|
|
247
|
-
void cur;
|
|
248
|
-
void taskMap;
|
|
249
244
|
}
|
|
250
245
|
return { criticalTasks, criticalLinks };
|
|
251
246
|
}
|
|
@@ -619,6 +614,29 @@ function computeProgressRollup(node) {
|
|
|
619
614
|
}
|
|
620
615
|
node.computedProgress = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
621
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
|
+
}
|
|
622
640
|
function flatten(roots) {
|
|
623
641
|
const out = [];
|
|
624
642
|
const stack = [];
|
|
@@ -640,6 +658,9 @@ function flatten(roots) {
|
|
|
640
658
|
}
|
|
641
659
|
return out;
|
|
642
660
|
}
|
|
661
|
+
function estimateTextWidth(text, fontSize) {
|
|
662
|
+
return text.length * fontSize * 0.62;
|
|
663
|
+
}
|
|
643
664
|
function anchorPoint(bar, side) {
|
|
644
665
|
const y = bar.y + bar.height / 2;
|
|
645
666
|
return side === "start" ? { x: bar.x, y } : { x: bar.x + bar.width, y };
|
|
@@ -702,6 +723,14 @@ function linkPath(from, to) {
|
|
|
702
723
|
const c2y = to.y;
|
|
703
724
|
return `M ${from.x} ${from.y} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${to.x} ${to.y}`;
|
|
704
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
|
+
};
|
|
705
734
|
function computeLayout(input) {
|
|
706
735
|
const {
|
|
707
736
|
tasks,
|
|
@@ -717,7 +746,8 @@ function computeLayout(input) {
|
|
|
717
746
|
markers = [],
|
|
718
747
|
autoRollupProgress = false,
|
|
719
748
|
selectedTaskIds,
|
|
720
|
-
pagination
|
|
749
|
+
pagination,
|
|
750
|
+
sort = null
|
|
721
751
|
} = input;
|
|
722
752
|
validateInputs(tasks, dependencies);
|
|
723
753
|
const { roots, nodeById } = buildTree(tasks);
|
|
@@ -725,6 +755,7 @@ function computeLayout(input) {
|
|
|
725
755
|
if (autoRollupProgress) {
|
|
726
756
|
for (const root of roots) computeProgressRollup(root);
|
|
727
757
|
}
|
|
758
|
+
sortTree(roots, columns, sort);
|
|
728
759
|
let visibleRoots = roots;
|
|
729
760
|
if (pagination && pagination.pageSize > 0) {
|
|
730
761
|
const start = Math.max(0, (pagination.page - 1) * pagination.pageSize);
|
|
@@ -744,8 +775,15 @@ function computeLayout(input) {
|
|
|
744
775
|
const now = Date.now();
|
|
745
776
|
const rawRangeStart = rangeMin === null ? new Date(now) : new Date(rangeMin);
|
|
746
777
|
const rawRangeEnd = rangeMax === null ? new Date(now + 1) : new Date(rangeMax);
|
|
747
|
-
|
|
748
|
-
|
|
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
|
+
}
|
|
749
787
|
const rows = [];
|
|
750
788
|
const bars = [];
|
|
751
789
|
const barByTaskId = /* @__PURE__ */ new Map();
|
|
@@ -765,8 +803,9 @@ function computeLayout(input) {
|
|
|
765
803
|
const end = row.node.computedEnd;
|
|
766
804
|
if (!start || !end) return;
|
|
767
805
|
const x = dateToX(start, paddedStart, viewMode, columnWidth);
|
|
768
|
-
const
|
|
769
|
-
if (!Number.isFinite(x) || !Number.isFinite(
|
|
806
|
+
const rawWidth = Math.max(0, dateToX(end, paddedStart, viewMode, columnWidth) - x);
|
|
807
|
+
if (!Number.isFinite(x) || !Number.isFinite(rawWidth)) return;
|
|
808
|
+
const width2 = rawWidth > 0 ? Math.max(rawWidth, 3) : rawWidth;
|
|
770
809
|
const progress = task.progress !== void 0 ? Math.min(100, Math.max(0, task.progress)) : autoRollupProgress ? row.node.computedProgress : 0;
|
|
771
810
|
const barHeight = theme.barHeight;
|
|
772
811
|
const barY = y + (theme.rowHeight - barHeight) / 2;
|
|
@@ -874,7 +913,13 @@ function computeLayout(input) {
|
|
|
874
913
|
})).filter((m) => Number.isFinite(m.x));
|
|
875
914
|
let width = 0;
|
|
876
915
|
for (const tick of ticks) width = Math.max(width, tick.x);
|
|
877
|
-
for (const bar of bars)
|
|
916
|
+
for (const bar of bars) {
|
|
917
|
+
width = Math.max(width, bar.x + bar.width);
|
|
918
|
+
if (bar.label) {
|
|
919
|
+
const labelOffset = bar.isMilestone ? theme.barHeight * 0.4 + 8 : bar.width + 6;
|
|
920
|
+
width = Math.max(width, bar.x + labelOffset + estimateTextWidth(bar.label, theme.fontSize));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
878
923
|
width += columnWidth;
|
|
879
924
|
const height = rows.length * theme.rowHeight;
|
|
880
925
|
return {
|
|
@@ -889,7 +934,8 @@ function computeLayout(input) {
|
|
|
889
934
|
columns,
|
|
890
935
|
theme,
|
|
891
936
|
markers: renderMarkers,
|
|
892
|
-
rangeStart: paddedStart
|
|
937
|
+
rangeStart: paddedStart,
|
|
938
|
+
sort
|
|
893
939
|
};
|
|
894
940
|
}
|
|
895
941
|
|
|
@@ -1086,6 +1132,7 @@ var GanttRenderer = class {
|
|
|
1086
1132
|
this.lastModel = null;
|
|
1087
1133
|
this.lastTasks = [];
|
|
1088
1134
|
this.lastColumns = [];
|
|
1135
|
+
this.lastExplicitThemeKeys = /* @__PURE__ */ new Set();
|
|
1089
1136
|
this.onGridDoubleClick = (evt) => {
|
|
1090
1137
|
if (!this.options.onRenameCommit) return;
|
|
1091
1138
|
const target = evt.target;
|
|
@@ -1123,8 +1170,13 @@ var GanttRenderer = class {
|
|
|
1123
1170
|
this.root = el("div", "gantt-root");
|
|
1124
1171
|
this.gridPanel = el("div", "gantt-grid-panel");
|
|
1125
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);
|
|
1126
1177
|
this.svg = svgEl("svg");
|
|
1127
1178
|
this.svg.classList.add("gantt-svg");
|
|
1179
|
+
this.timelineScroll.appendChild(this.headerContainer);
|
|
1128
1180
|
this.timelineScroll.appendChild(this.svg);
|
|
1129
1181
|
this.root.appendChild(this.gridPanel);
|
|
1130
1182
|
this.root.appendChild(this.timelineScroll);
|
|
@@ -1140,9 +1192,14 @@ var GanttRenderer = class {
|
|
|
1140
1192
|
this.lastTasks = tasks;
|
|
1141
1193
|
this.lastColumns = columns;
|
|
1142
1194
|
const cssVars = explicitThemeToCssVars(explicitTheme);
|
|
1143
|
-
|
|
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) {
|
|
1144
1200
|
this.root.style.setProperty(key, cssVars[key]);
|
|
1145
1201
|
}
|
|
1202
|
+
this.lastExplicitThemeKeys = newKeys;
|
|
1146
1203
|
this.renderGridPanel(model, tasks, columns);
|
|
1147
1204
|
this.renderTimeline(model, tasks);
|
|
1148
1205
|
}
|
|
@@ -1170,7 +1227,7 @@ var GanttRenderer = class {
|
|
|
1170
1227
|
const header = el("div", "gantt-grid-header-row");
|
|
1171
1228
|
header.style.height = `${model.headerHeight}px`;
|
|
1172
1229
|
for (const col of columns) {
|
|
1173
|
-
header.appendChild(this.renderHeaderCell(col, columns));
|
|
1230
|
+
header.appendChild(this.renderHeaderCell(col, columns, model.sort));
|
|
1174
1231
|
}
|
|
1175
1232
|
this.gridPanel.appendChild(header);
|
|
1176
1233
|
const body = el("div", "gantt-grid-body");
|
|
@@ -1183,11 +1240,23 @@ var GanttRenderer = class {
|
|
|
1183
1240
|
});
|
|
1184
1241
|
this.gridPanel.appendChild(body);
|
|
1185
1242
|
}
|
|
1186
|
-
renderHeaderCell(col, columns) {
|
|
1243
|
+
renderHeaderCell(col, columns, sort) {
|
|
1187
1244
|
const cell = el("div", "gantt-grid-header-cell");
|
|
1188
1245
|
cell.style.position = "relative";
|
|
1189
1246
|
if (col.width) cell.style.width = `${col.width}px`;
|
|
1190
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
|
+
}
|
|
1191
1260
|
if (this.options.onColumnReorder) {
|
|
1192
1261
|
cell.draggable = true;
|
|
1193
1262
|
cell.style.cursor = "grab";
|
|
@@ -1314,8 +1383,8 @@ var GanttRenderer = class {
|
|
|
1314
1383
|
renderTimeline(model, tasks) {
|
|
1315
1384
|
this.svg.replaceChildren();
|
|
1316
1385
|
this.svg.setAttribute("width", String(model.width));
|
|
1317
|
-
this.svg.setAttribute("height", String(model.
|
|
1318
|
-
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}`);
|
|
1319
1388
|
const defs = svgEl("defs");
|
|
1320
1389
|
const marker = svgEl("marker");
|
|
1321
1390
|
marker.setAttribute("id", this.markerDefsId);
|
|
@@ -1332,7 +1401,6 @@ var GanttRenderer = class {
|
|
|
1332
1401
|
this.svg.appendChild(defs);
|
|
1333
1402
|
const { startY, endY } = this.getViewportRowRange(model);
|
|
1334
1403
|
const bodyGroup = svgEl("g");
|
|
1335
|
-
bodyGroup.setAttribute("transform", `translate(0, ${model.headerHeight})`);
|
|
1336
1404
|
this.renderGridLines(bodyGroup, model);
|
|
1337
1405
|
this.renderRowBackgrounds(bodyGroup, model, startY, endY);
|
|
1338
1406
|
this.renderBars(bodyGroup, model, tasks, startY, endY);
|
|
@@ -1373,7 +1441,9 @@ var GanttRenderer = class {
|
|
|
1373
1441
|
}
|
|
1374
1442
|
}
|
|
1375
1443
|
renderMarkers(group, model) {
|
|
1376
|
-
|
|
1444
|
+
const sorted = [...model.markers].sort((a, b) => a.x - b.x);
|
|
1445
|
+
let defs = null;
|
|
1446
|
+
sorted.forEach((marker, i) => {
|
|
1377
1447
|
const line = svgEl("line");
|
|
1378
1448
|
line.setAttribute("x1", String(marker.x));
|
|
1379
1449
|
line.setAttribute("x2", String(marker.x));
|
|
@@ -1385,21 +1455,41 @@ var GanttRenderer = class {
|
|
|
1385
1455
|
line.dataset.ganttMarker = marker.label;
|
|
1386
1456
|
group.appendChild(line);
|
|
1387
1457
|
if (marker.label) {
|
|
1458
|
+
const nextX = sorted[i + 1]?.x ?? model.width;
|
|
1459
|
+
const cellWidth = Math.max(0, nextX - marker.x);
|
|
1460
|
+
if (!defs) {
|
|
1461
|
+
defs = svgEl("defs");
|
|
1462
|
+
group.insertBefore(defs, group.firstChild);
|
|
1463
|
+
}
|
|
1464
|
+
const clipId = `${this.markerDefsId}-marker-${i}`;
|
|
1465
|
+
const clipPath = svgEl("clipPath");
|
|
1466
|
+
clipPath.setAttribute("id", clipId);
|
|
1467
|
+
const clipRect = svgEl("rect");
|
|
1468
|
+
clipRect.setAttribute("x", String(marker.x));
|
|
1469
|
+
clipRect.setAttribute("y", "0");
|
|
1470
|
+
clipRect.setAttribute("width", String(cellWidth));
|
|
1471
|
+
clipRect.setAttribute("height", String(model.height));
|
|
1472
|
+
clipPath.appendChild(clipRect);
|
|
1473
|
+
defs.appendChild(clipPath);
|
|
1388
1474
|
const label = svgEl("text");
|
|
1389
1475
|
label.setAttribute("x", String(marker.x + 4));
|
|
1390
1476
|
label.setAttribute("y", "12");
|
|
1477
|
+
label.setAttribute("clip-path", `url(#${clipId})`);
|
|
1391
1478
|
label.setAttribute("fill", marker.color);
|
|
1392
1479
|
label.setAttribute("font-size", String(model.theme.fontSize));
|
|
1393
1480
|
label.setAttribute("font-family", model.theme.fontFamily);
|
|
1394
1481
|
label.textContent = marker.label;
|
|
1395
1482
|
group.appendChild(label);
|
|
1396
1483
|
}
|
|
1397
|
-
}
|
|
1484
|
+
});
|
|
1398
1485
|
}
|
|
1399
1486
|
renderHeader(model) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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");
|
|
1403
1493
|
headerGroup.setAttribute("class", "gantt-header-group");
|
|
1404
1494
|
const bg = svgEl("rect");
|
|
1405
1495
|
bg.setAttribute("x", "0");
|
|
@@ -1408,17 +1498,33 @@ var GanttRenderer = class {
|
|
|
1408
1498
|
bg.setAttribute("height", String(model.headerHeight));
|
|
1409
1499
|
bg.style.fill = themedFill("backgroundColor", model.theme.backgroundColor);
|
|
1410
1500
|
headerGroup.appendChild(bg);
|
|
1411
|
-
|
|
1501
|
+
const defs = svgEl("defs");
|
|
1502
|
+
const ticks = model.ticks;
|
|
1503
|
+
for (let i = 0; i < ticks.length; i++) {
|
|
1504
|
+
const tick = ticks[i];
|
|
1505
|
+
const cellWidth = (ticks[i + 1]?.x ?? model.width) - tick.x;
|
|
1506
|
+
const clipId = `${this.markerDefsId}-tick-${i}`;
|
|
1507
|
+
const clipPath = svgEl("clipPath");
|
|
1508
|
+
clipPath.setAttribute("id", clipId);
|
|
1509
|
+
const clipRect = svgEl("rect");
|
|
1510
|
+
clipRect.setAttribute("x", String(tick.x));
|
|
1511
|
+
clipRect.setAttribute("y", "0");
|
|
1512
|
+
clipRect.setAttribute("width", String(Math.max(0, cellWidth)));
|
|
1513
|
+
clipRect.setAttribute("height", String(model.headerHeight));
|
|
1514
|
+
clipPath.appendChild(clipRect);
|
|
1515
|
+
defs.appendChild(clipPath);
|
|
1412
1516
|
const text = svgEl("text");
|
|
1413
1517
|
text.setAttribute("x", String(tick.x + 4));
|
|
1414
1518
|
text.setAttribute("y", String(model.headerHeight - 8));
|
|
1519
|
+
text.setAttribute("clip-path", `url(#${clipId})`);
|
|
1415
1520
|
text.style.fill = themedFill("textColor", model.theme.textColor);
|
|
1416
1521
|
text.setAttribute("font-size", String(model.theme.fontSize));
|
|
1417
1522
|
text.setAttribute("font-family", model.theme.fontFamily);
|
|
1418
1523
|
text.textContent = tick.label;
|
|
1419
1524
|
headerGroup.appendChild(text);
|
|
1420
1525
|
}
|
|
1421
|
-
|
|
1526
|
+
headerGroup.insertBefore(defs, headerGroup.firstChild);
|
|
1527
|
+
this.headerSvg.appendChild(headerGroup);
|
|
1422
1528
|
}
|
|
1423
1529
|
/**
|
|
1424
1530
|
* Invisible full-width hit-test rects, one per row, behind the bars. Lets
|
|
@@ -1453,11 +1559,6 @@ var GanttRenderer = class {
|
|
|
1453
1559
|
g.setAttribute("class", "gantt-bar");
|
|
1454
1560
|
g.dataset.ganttBar = bar.taskId;
|
|
1455
1561
|
g.setAttribute("transform", `translate(${bar.x}, ${bar.y})`);
|
|
1456
|
-
if (task?.notes) {
|
|
1457
|
-
const title = svgEl("title");
|
|
1458
|
-
title.textContent = task.notes;
|
|
1459
|
-
g.appendChild(title);
|
|
1460
|
-
}
|
|
1461
1562
|
if (this.options.keyboardAccessible) {
|
|
1462
1563
|
g.setAttribute("tabindex", "0");
|
|
1463
1564
|
g.setAttribute("role", "button");
|
|
@@ -1702,8 +1803,30 @@ var GanttRenderer = class {
|
|
|
1702
1803
|
group.appendChild(path);
|
|
1703
1804
|
}
|
|
1704
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
|
+
*/
|
|
1705
1811
|
toSVGString() {
|
|
1706
|
-
|
|
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);
|
|
1707
1830
|
}
|
|
1708
1831
|
destroy() {
|
|
1709
1832
|
if (this.scrollListener) {
|
|
@@ -2082,13 +2205,17 @@ var InteractionController = class {
|
|
|
2082
2205
|
}
|
|
2083
2206
|
};
|
|
2084
2207
|
this.onSvgDoubleClick = (evt) => {
|
|
2085
|
-
if (!this.callbacks.onLinkDblClick) return;
|
|
2086
2208
|
const target = evt.target;
|
|
2087
2209
|
const linkPath2 = target.closest("[data-gantt-link]");
|
|
2088
2210
|
const key = linkPath2?.dataset.ganttLink;
|
|
2089
|
-
if (
|
|
2090
|
-
|
|
2091
|
-
|
|
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);
|
|
2092
2219
|
};
|
|
2093
2220
|
this.onPointerDown = (evt) => {
|
|
2094
2221
|
const target = evt.target;
|
|
@@ -2351,6 +2478,119 @@ var InteractionController = class {
|
|
|
2351
2478
|
}
|
|
2352
2479
|
};
|
|
2353
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
|
+
|
|
2354
2594
|
// src/index.ts
|
|
2355
2595
|
var HAS_DOM = typeof document !== "undefined" && typeof window !== "undefined";
|
|
2356
2596
|
var DEFAULT_COLUMNS = [{ id: "name", title: "Name" }];
|
|
@@ -2370,6 +2610,9 @@ var _GanttChart = class _GanttChart {
|
|
|
2370
2610
|
this.history = null;
|
|
2371
2611
|
this.renderer = null;
|
|
2372
2612
|
this.interactions = null;
|
|
2613
|
+
this.tooltip = null;
|
|
2614
|
+
this.resizeObserver = null;
|
|
2615
|
+
this.sort = null;
|
|
2373
2616
|
this.renderModel = null;
|
|
2374
2617
|
this.rafHandle = null;
|
|
2375
2618
|
this.currentPage = 1;
|
|
@@ -2392,6 +2635,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2392
2635
|
showCriticalPath: options.showCriticalPath ?? false,
|
|
2393
2636
|
showBaseline: options.showBaseline ?? false,
|
|
2394
2637
|
showDeadlines: options.showDeadlines ?? true,
|
|
2638
|
+
showTooltip: options.showTooltip ?? true,
|
|
2639
|
+
autoFitToViewport: options.autoFitToViewport ?? false,
|
|
2395
2640
|
showAssigneeAvatars: options.showAssigneeAvatars ?? false,
|
|
2396
2641
|
enableHistory: options.enableHistory ?? false,
|
|
2397
2642
|
keyboardAccessible: options.keyboardAccessible ?? false,
|
|
@@ -2412,13 +2657,16 @@ var _GanttChart = class _GanttChart {
|
|
|
2412
2657
|
onDependencyRemove: options.onDependencyRemove,
|
|
2413
2658
|
onDependencyDblClick: options.onDependencyDblClick,
|
|
2414
2659
|
onTaskClick: options.onTaskClick,
|
|
2660
|
+
onTaskDblClick: options.onTaskDblClick,
|
|
2415
2661
|
onGroupToggle: options.onGroupToggle,
|
|
2416
2662
|
onContextMenu: options.onContextMenu,
|
|
2417
2663
|
onTaskCreate: options.onTaskCreate,
|
|
2418
2664
|
onColumnResize: options.onColumnResize,
|
|
2419
2665
|
onColumnReorder: options.onColumnReorder,
|
|
2420
2666
|
onTaskReorder: options.onTaskReorder,
|
|
2421
|
-
onSelectionChange: options.onSelectionChange
|
|
2667
|
+
onSelectionChange: options.onSelectionChange,
|
|
2668
|
+
onSortChange: options.onSortChange,
|
|
2669
|
+
renderTooltip: options.renderTooltip
|
|
2422
2670
|
};
|
|
2423
2671
|
if (this.options.enableHistory) {
|
|
2424
2672
|
this.history = new HistoryManager((state) => this.emitter.emit("history-change", state));
|
|
@@ -2441,7 +2689,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2441
2689
|
onRenameCommit: (taskId, name) => this.updateTask(taskId, { name }),
|
|
2442
2690
|
onColumnResize: this.options.onColumnResize ? (columnId, width) => this.handleColumnResize(columnId, width) : void 0,
|
|
2443
2691
|
onColumnReorder: this.options.onColumnReorder ? (order) => this.handleColumnReorder(order) : void 0,
|
|
2444
|
-
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)
|
|
2445
2694
|
});
|
|
2446
2695
|
this.interactions = new InteractionController(
|
|
2447
2696
|
this.renderer.svg,
|
|
@@ -2462,7 +2711,17 @@ var _GanttChart = class _GanttChart {
|
|
|
2462
2711
|
onCreateTaskDrag: this.options.onTaskCreate ? (rowTaskId, startXPx, endXPx) => this.handleCreateTaskDrag(rowTaskId, startXPx, endXPx) : void 0,
|
|
2463
2712
|
onLinkDblClick: this.options.onDependencyDblClick ? (fromId, toId) => {
|
|
2464
2713
|
const dep = this.dependencies.find((d) => d.fromId === fromId && d.toId === toId);
|
|
2465
|
-
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
|
+
}
|
|
2466
2725
|
} : void 0,
|
|
2467
2726
|
onContextMenu: this.options.onContextMenu ? (taskId, evt) => {
|
|
2468
2727
|
const task = this.tasks.find((t) => t.id === taskId);
|
|
@@ -2481,6 +2740,17 @@ var _GanttChart = class _GanttChart {
|
|
|
2481
2740
|
pxPerMs: () => pxPerMs(this.options.viewMode, this.columnWidth)
|
|
2482
2741
|
}
|
|
2483
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
|
+
}
|
|
2484
2754
|
}
|
|
2485
2755
|
handleMove(taskId, dxMs) {
|
|
2486
2756
|
if (dxMs === 0) return;
|
|
@@ -2501,7 +2771,7 @@ var _GanttChart = class _GanttChart {
|
|
|
2501
2771
|
const newStart = edge === "left" ? new Date(task.start.getTime() + dxMs) : task.start;
|
|
2502
2772
|
const newEnd = edge === "right" ? new Date(task.end.getTime() + dxMs) : task.end;
|
|
2503
2773
|
if (newEnd.getTime() <= newStart.getTime()) return;
|
|
2504
|
-
|
|
2774
|
+
const newSegments = task.segments?.map((s) => ({ ...s }));
|
|
2505
2775
|
if (newSegments && newSegments.length > 0) {
|
|
2506
2776
|
if (edge === "left") newSegments[0] = { ...newSegments[0], start: newStart };
|
|
2507
2777
|
else newSegments[newSegments.length - 1] = { ...newSegments[newSegments.length - 1], end: newEnd };
|
|
@@ -2885,6 +3155,24 @@ var _GanttChart = class _GanttChart {
|
|
|
2885
3155
|
this.options.onColumnReorder?.(order);
|
|
2886
3156
|
this.scheduleRender();
|
|
2887
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
|
+
}
|
|
2888
3176
|
handleRowReorder(draggedId, targetId, position) {
|
|
2889
3177
|
const draggedIndex = this.tasks.findIndex((t) => t.id === draggedId);
|
|
2890
3178
|
const targetIndex = this.tasks.findIndex((t) => t.id === targetId);
|
|
@@ -2951,13 +3239,18 @@ var _GanttChart = class _GanttChart {
|
|
|
2951
3239
|
this.scheduleRender();
|
|
2952
3240
|
}
|
|
2953
3241
|
setOptions(partial) {
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
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) {
|
|
2957
3247
|
this.theme = mergeTheme(this.explicitTheme, this.colorScheme);
|
|
2958
3248
|
}
|
|
2959
|
-
if (partial
|
|
3249
|
+
if ("columns" in partial) this.columns = partial.columns ?? this.columns;
|
|
2960
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
|
+
}
|
|
2961
3254
|
Object.assign(this.options, partial);
|
|
2962
3255
|
this.scheduleRender();
|
|
2963
3256
|
}
|
|
@@ -3060,7 +3353,8 @@ var _GanttChart = class _GanttChart {
|
|
|
3060
3353
|
markers: this.options.markers,
|
|
3061
3354
|
autoRollupProgress: this.options.autoRollupProgress,
|
|
3062
3355
|
selectedTaskIds: this.options.selectable ? this.selectedTaskIds : void 0,
|
|
3063
|
-
pagination
|
|
3356
|
+
pagination,
|
|
3357
|
+
sort: this.sort
|
|
3064
3358
|
});
|
|
3065
3359
|
}
|
|
3066
3360
|
scheduleRender(immediate = false) {
|
|
@@ -3075,6 +3369,7 @@ var _GanttChart = class _GanttChart {
|
|
|
3075
3369
|
});
|
|
3076
3370
|
}
|
|
3077
3371
|
doRender() {
|
|
3372
|
+
if (this.options.autoFitToViewport) this.applyAutoFit();
|
|
3078
3373
|
this.renderModel = this.computeModel();
|
|
3079
3374
|
this.renderer?.render(this.renderModel, this.tasks, this.columns, this.explicitTheme);
|
|
3080
3375
|
if (this.renderer && this.colorScheme !== "auto") {
|
|
@@ -3083,6 +3378,29 @@ var _GanttChart = class _GanttChart {
|
|
|
3083
3378
|
this.renderer?.root.removeAttribute("data-gantt-theme");
|
|
3084
3379
|
}
|
|
3085
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
|
+
}
|
|
3086
3404
|
undo() {
|
|
3087
3405
|
this.history?.undo();
|
|
3088
3406
|
}
|
|
@@ -3177,6 +3495,8 @@ var _GanttChart = class _GanttChart {
|
|
|
3177
3495
|
this.darkMediaQuery?.removeEventListener?.("change", this.handleSchemeChange);
|
|
3178
3496
|
this.emitter.removeAllListeners();
|
|
3179
3497
|
this.interactions?.destroy();
|
|
3498
|
+
this.tooltip?.destroy();
|
|
3499
|
+
this.resizeObserver?.disconnect();
|
|
3180
3500
|
this.renderer?.destroy();
|
|
3181
3501
|
}
|
|
3182
3502
|
};
|