@ganttloom/gantt-core 0.4.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +300 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +300 -89
- package/dist/index.js.map +1 -1
- package/dist/styles.css +112 -5
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -74,7 +74,8 @@ var DEFAULT_THEME = {
|
|
|
74
74
|
backgroundColor: "#ffffff",
|
|
75
75
|
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
76
76
|
fontSize: 12,
|
|
77
|
-
borderRadius: 4
|
|
77
|
+
borderRadius: 4,
|
|
78
|
+
borderColor: "#e2e8f0"
|
|
78
79
|
};
|
|
79
80
|
var DARK_THEME = {
|
|
80
81
|
...DEFAULT_THEME,
|
|
@@ -91,7 +92,8 @@ var DARK_THEME = {
|
|
|
91
92
|
markerColor: "#a78bfa",
|
|
92
93
|
selectionColor: "#38bdf8",
|
|
93
94
|
textColor: "#e5e7eb",
|
|
94
|
-
backgroundColor: "#181c24"
|
|
95
|
+
backgroundColor: "#181c24",
|
|
96
|
+
borderColor: "#334155"
|
|
95
97
|
};
|
|
96
98
|
var DENSITY_PRESETS = {
|
|
97
99
|
compact: { rowHeight: 26, barHeight: 16, headerHeight: 38, fontSize: 11 },
|
|
@@ -130,7 +132,8 @@ var CSS_VAR_NAMES = {
|
|
|
130
132
|
backgroundColor: "--gantt-background-color",
|
|
131
133
|
fontFamily: "--gantt-font-family",
|
|
132
134
|
fontSize: "--gantt-font-size",
|
|
133
|
-
borderRadius: "--gantt-border-radius"
|
|
135
|
+
borderRadius: "--gantt-border-radius",
|
|
136
|
+
borderColor: "--gantt-border-color"
|
|
134
137
|
};
|
|
135
138
|
var PX_FIELDS = /* @__PURE__ */ new Set([
|
|
136
139
|
"rowHeight",
|
|
@@ -556,20 +559,38 @@ function labelFor(date, viewMode) {
|
|
|
556
559
|
const h = date.getHours();
|
|
557
560
|
const period = h >= 12 ? "PM" : "AM";
|
|
558
561
|
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
559
|
-
return
|
|
562
|
+
return {
|
|
563
|
+
label: `${h12} ${period}`,
|
|
564
|
+
parentLabel: `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()} ${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
|
|
565
|
+
};
|
|
560
566
|
}
|
|
561
567
|
case "day":
|
|
562
|
-
return
|
|
568
|
+
return {
|
|
569
|
+
label: `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()}`,
|
|
570
|
+
parentLabel: `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
|
|
571
|
+
};
|
|
563
572
|
case "week":
|
|
564
|
-
return
|
|
573
|
+
return {
|
|
574
|
+
label: `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`,
|
|
575
|
+
parentLabel: `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
|
|
576
|
+
};
|
|
565
577
|
case "month":
|
|
566
|
-
return
|
|
578
|
+
return {
|
|
579
|
+
label: MONTH_LABELS[date.getMonth()] ?? "",
|
|
580
|
+
parentLabel: `${date.getFullYear()}`
|
|
581
|
+
};
|
|
567
582
|
case "quarter": {
|
|
568
583
|
const q = Math.floor(date.getMonth() / 3) + 1;
|
|
569
|
-
return
|
|
584
|
+
return {
|
|
585
|
+
label: `Q${q}`,
|
|
586
|
+
parentLabel: `${date.getFullYear()}`
|
|
587
|
+
};
|
|
570
588
|
}
|
|
571
589
|
case "year":
|
|
572
|
-
return
|
|
590
|
+
return {
|
|
591
|
+
label: `${date.getFullYear()}`,
|
|
592
|
+
parentLabel: ""
|
|
593
|
+
};
|
|
573
594
|
}
|
|
574
595
|
}
|
|
575
596
|
function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @__PURE__ */ new Date(), calendar) {
|
|
@@ -583,10 +604,12 @@ function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @
|
|
|
583
604
|
while (cursor.getTime() < rangeEnd.getTime() && guard < MAX_TICKS) {
|
|
584
605
|
guard++;
|
|
585
606
|
const x = (cursor.getTime() - rangeStart.getTime()) * scale;
|
|
607
|
+
const labels = labelFor(cursor, viewMode);
|
|
586
608
|
ticks.push({
|
|
587
609
|
date: new Date(cursor.getTime()),
|
|
588
610
|
x,
|
|
589
|
-
label:
|
|
611
|
+
label: labels.label,
|
|
612
|
+
parentLabel: labels.parentLabel,
|
|
590
613
|
isWeekend: dayLevel ? isWeekend(cursor) : false,
|
|
591
614
|
isNonWorking: viewMode === "hour" && calendar?.workingHours ? !isWorkingTime(cursor, calendar) : dayLevel ? isNonWorkingDay(cursor, calendar) : false,
|
|
592
615
|
isToday: isSameDay(cursor, today) && dayLevel,
|
|
@@ -827,11 +850,14 @@ function computeLayout(input) {
|
|
|
827
850
|
let paddedStart = addUnit(rawRangeStart, viewMode, -1);
|
|
828
851
|
let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
|
|
829
852
|
const minUnits = MIN_VISIBLE_UNITS[viewMode];
|
|
853
|
+
let targetMinUnits = minUnits;
|
|
854
|
+
if (input.timelineViewportWidth && input.timelineViewportWidth > 0) {
|
|
855
|
+
targetMinUnits = Math.max(targetMinUnits, Math.ceil(input.timelineViewportWidth / columnWidth));
|
|
856
|
+
}
|
|
830
857
|
const currentUnits = (paddedEnd.getTime() - paddedStart.getTime()) / approxUnitMs(viewMode);
|
|
831
|
-
if (currentUnits <
|
|
832
|
-
const deficit =
|
|
833
|
-
|
|
834
|
-
paddedEnd = addUnit(paddedEnd, viewMode, Math.floor(deficit / 2));
|
|
858
|
+
if (currentUnits < targetMinUnits) {
|
|
859
|
+
const deficit = targetMinUnits - currentUnits;
|
|
860
|
+
paddedEnd = addUnit(paddedEnd, viewMode, Math.ceil(deficit));
|
|
835
861
|
}
|
|
836
862
|
const rows = [];
|
|
837
863
|
const bars = [];
|
|
@@ -949,6 +975,7 @@ function computeLayout(input) {
|
|
|
949
975
|
(t) => ({
|
|
950
976
|
x: t.x,
|
|
951
977
|
label: t.label,
|
|
978
|
+
parentLabel: t.parentLabel,
|
|
952
979
|
isWeekend: t.isWeekend,
|
|
953
980
|
isNonWorking: t.isNonWorking,
|
|
954
981
|
isToday: t.isToday,
|
|
@@ -1226,6 +1253,9 @@ var GanttRenderer = class {
|
|
|
1226
1253
|
this.svg = svgEl("svg");
|
|
1227
1254
|
this.svg.classList.add("gantt-svg");
|
|
1228
1255
|
this.timelineScroll.appendChild(this.headerContainer);
|
|
1256
|
+
if (this.options.readonly) {
|
|
1257
|
+
this.root.classList.add("gantt-readonly");
|
|
1258
|
+
}
|
|
1229
1259
|
this.timelineScroll.appendChild(this.svg);
|
|
1230
1260
|
this.root.appendChild(this.gridPanel);
|
|
1231
1261
|
this.root.appendChild(this.timelineScroll);
|
|
@@ -1293,7 +1323,10 @@ var GanttRenderer = class {
|
|
|
1293
1323
|
const cell = el("div", "gantt-grid-header-cell");
|
|
1294
1324
|
cell.style.position = "relative";
|
|
1295
1325
|
if (col.width) cell.style.width = `${col.width}px`;
|
|
1296
|
-
|
|
1326
|
+
const titleSpan = document.createElement("span");
|
|
1327
|
+
titleSpan.className = "gantt-cell-text";
|
|
1328
|
+
titleSpan.textContent = col.title;
|
|
1329
|
+
cell.appendChild(titleSpan);
|
|
1297
1330
|
if (col.sortable) {
|
|
1298
1331
|
cell.classList.add("gantt-grid-header-cell-sortable");
|
|
1299
1332
|
const active = sort?.columnId === col.id;
|
|
@@ -1335,7 +1368,7 @@ var GanttRenderer = class {
|
|
|
1335
1368
|
evt.preventDefault();
|
|
1336
1369
|
evt.stopPropagation();
|
|
1337
1370
|
const startX = evt.clientX;
|
|
1338
|
-
const startWidth =
|
|
1371
|
+
const startWidth = cell.getBoundingClientRect().width;
|
|
1339
1372
|
const onMove = (moveEvt) => {
|
|
1340
1373
|
const newWidth = Math.max(30, startWidth + (moveEvt.clientX - startX));
|
|
1341
1374
|
cell.style.width = `${newWidth}px`;
|
|
@@ -1388,9 +1421,11 @@ var GanttRenderer = class {
|
|
|
1388
1421
|
const cell = el("div", "gantt-grid-cell");
|
|
1389
1422
|
if (col.width) cell.style.width = `${col.width}px`;
|
|
1390
1423
|
if (col.align) cell.style.textAlign = col.align;
|
|
1391
|
-
const
|
|
1424
|
+
const isTreeColumn = col.id === "name" || !columns.some((c) => c.id === "name") && colIndex === 0;
|
|
1425
|
+
console.log(`Column ${col.id}: isTreeColumn=${isTreeColumn}, colIndex=${colIndex}, hasNameCol=${columns.some((c) => c.id === "name")}`);
|
|
1426
|
+
const rawValue = col.accessor ? col.accessor(task) : isTreeColumn ? task.name : task[col.id] ?? "";
|
|
1392
1427
|
const textValue = rawValue === null || rawValue === void 0 ? "" : String(rawValue);
|
|
1393
|
-
if (
|
|
1428
|
+
if (isTreeColumn) {
|
|
1394
1429
|
cell.dataset.ganttNameCell = task.id;
|
|
1395
1430
|
cell.dataset.ganttNameValue = textValue;
|
|
1396
1431
|
const indent = el("span", "gantt-indent");
|
|
@@ -1399,17 +1434,27 @@ var GanttRenderer = class {
|
|
|
1399
1434
|
if (row.hasChildren) {
|
|
1400
1435
|
const toggle = el("button", "gantt-toggle");
|
|
1401
1436
|
toggle.type = "button";
|
|
1437
|
+
toggle.className = `gantt-toggle ${row.collapsed ? "collapsed" : "expanded"}`;
|
|
1402
1438
|
toggle.dataset.ganttToggle = task.id;
|
|
1403
1439
|
toggle.setAttribute("aria-label", row.collapsed ? "Expand" : "Collapse");
|
|
1404
1440
|
toggle.textContent = row.collapsed ? "\u25B8" : "\u25BE";
|
|
1405
1441
|
cell.appendChild(toggle);
|
|
1406
1442
|
}
|
|
1443
|
+
const addChildBtn = el("button", "gantt-add-child");
|
|
1444
|
+
addChildBtn.type = "button";
|
|
1445
|
+
addChildBtn.dataset.ganttAddChild = task.id;
|
|
1446
|
+
addChildBtn.setAttribute("aria-label", "Add child task");
|
|
1447
|
+
addChildBtn.textContent = "+";
|
|
1448
|
+
cell.appendChild(addChildBtn);
|
|
1407
1449
|
}
|
|
1408
1450
|
const rendered = col.render ? col.render(task) : void 0;
|
|
1409
1451
|
if (rendered instanceof HTMLElement) {
|
|
1410
1452
|
cell.appendChild(rendered);
|
|
1411
1453
|
} else if (typeof rendered === "string") {
|
|
1412
|
-
|
|
1454
|
+
const span = document.createElement("span");
|
|
1455
|
+
span.className = "gantt-cell-text";
|
|
1456
|
+
span.textContent = rendered;
|
|
1457
|
+
cell.appendChild(span);
|
|
1413
1458
|
} else {
|
|
1414
1459
|
const href = col.getHref ? col.getHref(task) : null;
|
|
1415
1460
|
if (href && isSafeHref(href)) {
|
|
@@ -1418,10 +1463,13 @@ var GanttRenderer = class {
|
|
|
1418
1463
|
anchor.target = col.linkTarget ?? "_blank";
|
|
1419
1464
|
anchor.rel = "noopener noreferrer";
|
|
1420
1465
|
anchor.textContent = textValue;
|
|
1466
|
+
anchor.className = "gantt-cell-text";
|
|
1421
1467
|
cell.appendChild(anchor);
|
|
1422
1468
|
} else {
|
|
1423
|
-
const
|
|
1424
|
-
cell
|
|
1469
|
+
const span = document.createElement("span");
|
|
1470
|
+
span.className = "gantt-cell-text";
|
|
1471
|
+
span.textContent = textValue;
|
|
1472
|
+
cell.appendChild(span);
|
|
1425
1473
|
}
|
|
1426
1474
|
}
|
|
1427
1475
|
rowEl.appendChild(cell);
|
|
@@ -1452,8 +1500,8 @@ var GanttRenderer = class {
|
|
|
1452
1500
|
const bodyGroup = svgEl("g");
|
|
1453
1501
|
this.renderGridLines(bodyGroup, model);
|
|
1454
1502
|
this.renderRowBackgrounds(bodyGroup, model, startY, endY);
|
|
1455
|
-
this.renderBars(bodyGroup, model, tasks, startY, endY);
|
|
1456
1503
|
this.renderLinks(bodyGroup, model, startY, endY);
|
|
1504
|
+
this.renderBars(bodyGroup, model, tasks, startY, endY);
|
|
1457
1505
|
this.renderMarkers(bodyGroup, model);
|
|
1458
1506
|
this.svg.appendChild(bodyGroup);
|
|
1459
1507
|
this.renderHeader(model);
|
|
@@ -1520,15 +1568,21 @@ var GanttRenderer = class {
|
|
|
1520
1568
|
clipRect.setAttribute("height", String(model.height));
|
|
1521
1569
|
clipPath.appendChild(clipRect);
|
|
1522
1570
|
defs.appendChild(clipPath);
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1571
|
+
const fo = svgEl("foreignObject");
|
|
1572
|
+
fo.setAttribute("x", String(marker.x + 4));
|
|
1573
|
+
fo.setAttribute("y", "0");
|
|
1574
|
+
fo.setAttribute("width", String(cellWidth > 8 ? cellWidth - 8 : cellWidth));
|
|
1575
|
+
fo.setAttribute("height", String(model.height));
|
|
1576
|
+
const div = document.createElement("div");
|
|
1577
|
+
div.style.color = marker.color;
|
|
1578
|
+
div.style.fontSize = `${model.theme.fontSize}px`;
|
|
1579
|
+
div.style.fontFamily = model.theme.fontFamily;
|
|
1580
|
+
div.style.whiteSpace = "normal";
|
|
1581
|
+
div.style.overflowWrap = "break-word";
|
|
1582
|
+
div.style.paddingTop = "4px";
|
|
1583
|
+
div.textContent = marker.label;
|
|
1584
|
+
fo.appendChild(div);
|
|
1585
|
+
group.appendChild(fo);
|
|
1532
1586
|
}
|
|
1533
1587
|
});
|
|
1534
1588
|
}
|
|
@@ -1549,6 +1603,60 @@ var GanttRenderer = class {
|
|
|
1549
1603
|
headerGroup.appendChild(bg);
|
|
1550
1604
|
const defs = svgEl("defs");
|
|
1551
1605
|
const ticks = model.ticks;
|
|
1606
|
+
const parentGroups = [];
|
|
1607
|
+
let currentGroup = null;
|
|
1608
|
+
for (const tick of ticks) {
|
|
1609
|
+
if (!currentGroup || currentGroup.label !== tick.parentLabel) {
|
|
1610
|
+
currentGroup = { x: tick.x, label: tick.parentLabel };
|
|
1611
|
+
parentGroups.push(currentGroup);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const hasParentTier = parentGroups.some((g) => g.label !== "");
|
|
1615
|
+
const bottomTierY = hasParentTier ? model.headerHeight / 2 : 0;
|
|
1616
|
+
const tierHeight = hasParentTier ? model.headerHeight / 2 : model.headerHeight;
|
|
1617
|
+
if (hasParentTier) {
|
|
1618
|
+
for (let i = 0; i < parentGroups.length; i++) {
|
|
1619
|
+
const group = parentGroups[i];
|
|
1620
|
+
const nextX = parentGroups[i + 1]?.x ?? model.width;
|
|
1621
|
+
const cellWidth = nextX - group.x;
|
|
1622
|
+
const fo = svgEl("foreignObject");
|
|
1623
|
+
fo.setAttribute("x", String(group.x + 4));
|
|
1624
|
+
fo.setAttribute("y", "0");
|
|
1625
|
+
fo.setAttribute("width", String(Math.max(0, cellWidth - 4)));
|
|
1626
|
+
fo.setAttribute("height", String(tierHeight));
|
|
1627
|
+
const div = document.createElement("div");
|
|
1628
|
+
div.style.color = themedFill("textColor", model.theme.textColor);
|
|
1629
|
+
div.style.fontSize = `${model.theme.fontSize * 0.95}px`;
|
|
1630
|
+
div.style.fontFamily = model.theme.fontFamily;
|
|
1631
|
+
div.style.fontWeight = "600";
|
|
1632
|
+
div.style.whiteSpace = "nowrap";
|
|
1633
|
+
div.style.overflow = "hidden";
|
|
1634
|
+
div.style.textOverflow = "ellipsis";
|
|
1635
|
+
div.style.display = "flex";
|
|
1636
|
+
div.style.alignItems = "center";
|
|
1637
|
+
div.style.justifyContent = "center";
|
|
1638
|
+
div.style.height = "100%";
|
|
1639
|
+
div.textContent = group.label;
|
|
1640
|
+
fo.appendChild(div);
|
|
1641
|
+
headerGroup.appendChild(fo);
|
|
1642
|
+
const border = svgEl("line");
|
|
1643
|
+
border.setAttribute("x1", String(group.x));
|
|
1644
|
+
border.setAttribute("x2", String(nextX));
|
|
1645
|
+
border.setAttribute("y1", String(tierHeight));
|
|
1646
|
+
border.setAttribute("y2", String(tierHeight));
|
|
1647
|
+
border.style.stroke = themedFill("borderColor", model.theme.borderColor ?? "#e2e8f0");
|
|
1648
|
+
headerGroup.appendChild(border);
|
|
1649
|
+
if (i > 0) {
|
|
1650
|
+
const sep = svgEl("line");
|
|
1651
|
+
sep.setAttribute("x1", String(group.x));
|
|
1652
|
+
sep.setAttribute("x2", String(group.x));
|
|
1653
|
+
sep.setAttribute("y1", "0");
|
|
1654
|
+
sep.setAttribute("y2", String(tierHeight));
|
|
1655
|
+
sep.style.stroke = themedFill("borderColor", model.theme.borderColor ?? "#e2e8f0");
|
|
1656
|
+
headerGroup.appendChild(sep);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1552
1660
|
for (let i = 0; i < ticks.length; i++) {
|
|
1553
1661
|
const tick = ticks[i];
|
|
1554
1662
|
const cellWidth = (ticks[i + 1]?.x ?? model.width) - tick.x;
|
|
@@ -1557,20 +1665,30 @@ var GanttRenderer = class {
|
|
|
1557
1665
|
clipPath.setAttribute("id", clipId);
|
|
1558
1666
|
const clipRect = svgEl("rect");
|
|
1559
1667
|
clipRect.setAttribute("x", String(tick.x));
|
|
1560
|
-
clipRect.setAttribute("y",
|
|
1668
|
+
clipRect.setAttribute("y", String(bottomTierY));
|
|
1561
1669
|
clipRect.setAttribute("width", String(Math.max(0, cellWidth)));
|
|
1562
|
-
clipRect.setAttribute("height", String(
|
|
1670
|
+
clipRect.setAttribute("height", String(tierHeight));
|
|
1563
1671
|
clipPath.appendChild(clipRect);
|
|
1564
1672
|
defs.appendChild(clipPath);
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1673
|
+
const fo = svgEl("foreignObject");
|
|
1674
|
+
fo.setAttribute("x", String(tick.x + 4));
|
|
1675
|
+
fo.setAttribute("y", String(bottomTierY));
|
|
1676
|
+
fo.setAttribute("width", String(cellWidth > 8 ? cellWidth - 8 : cellWidth));
|
|
1677
|
+
fo.setAttribute("height", String(tierHeight));
|
|
1678
|
+
const div = document.createElement("div");
|
|
1679
|
+
div.style.color = themedFill("textColor", model.theme.textColor);
|
|
1680
|
+
div.style.fontSize = `${model.theme.fontSize}px`;
|
|
1681
|
+
div.style.fontFamily = model.theme.fontFamily;
|
|
1682
|
+
div.style.whiteSpace = "normal";
|
|
1683
|
+
div.style.overflowWrap = "break-word";
|
|
1684
|
+
div.style.display = "flex";
|
|
1685
|
+
div.style.alignItems = hasParentTier ? "center" : "flex-end";
|
|
1686
|
+
div.style.justifyContent = "center";
|
|
1687
|
+
div.style.paddingBottom = hasParentTier ? "0" : "4px";
|
|
1688
|
+
div.style.height = "100%";
|
|
1689
|
+
div.textContent = tick.label;
|
|
1690
|
+
fo.appendChild(div);
|
|
1691
|
+
headerGroup.appendChild(fo);
|
|
1574
1692
|
}
|
|
1575
1693
|
headerGroup.insertBefore(defs, headerGroup.firstChild);
|
|
1576
1694
|
this.headerSvg.appendChild(headerGroup);
|
|
@@ -1613,8 +1731,8 @@ var GanttRenderer = class {
|
|
|
1613
1731
|
g.setAttribute("role", "button");
|
|
1614
1732
|
if (task) {
|
|
1615
1733
|
const progress = Math.round(task.progress ?? 0);
|
|
1616
|
-
const
|
|
1617
|
-
g.setAttribute("aria-label",
|
|
1734
|
+
const label = `${task.name}, ${task.start.toDateString()} to ${task.end.toDateString()}, ${progress}% complete`;
|
|
1735
|
+
g.setAttribute("aria-label", label);
|
|
1618
1736
|
}
|
|
1619
1737
|
}
|
|
1620
1738
|
if (bar.baseline) {
|
|
@@ -1662,17 +1780,15 @@ var GanttRenderer = class {
|
|
|
1662
1780
|
`rotate(45, 0, ${bar.height / 2})`
|
|
1663
1781
|
);
|
|
1664
1782
|
g.appendChild(diamond);
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
g.appendChild(
|
|
1673
|
-
|
|
1674
|
-
}
|
|
1675
|
-
if (bar.segments && bar.segments.length > 0) {
|
|
1783
|
+
const label = svgEl("text");
|
|
1784
|
+
label.setAttribute("x", String(size / 2 + 8));
|
|
1785
|
+
label.setAttribute("y", String(bar.height / 2 + 4));
|
|
1786
|
+
label.style.fill = themedFill("textColor", model.theme.textColor);
|
|
1787
|
+
label.setAttribute("font-size", String(model.theme.fontSize));
|
|
1788
|
+
label.setAttribute("font-family", model.theme.fontFamily);
|
|
1789
|
+
label.textContent = bar.label;
|
|
1790
|
+
g.appendChild(label);
|
|
1791
|
+
} else if (bar.segments && bar.segments.length > 0) {
|
|
1676
1792
|
bar.segments.forEach((seg, i) => {
|
|
1677
1793
|
const segRect = svgEl("rect");
|
|
1678
1794
|
segRect.setAttribute("x", String(seg.x));
|
|
@@ -1745,29 +1861,31 @@ var GanttRenderer = class {
|
|
|
1745
1861
|
selectionOutline.setAttribute("stroke-width", "2");
|
|
1746
1862
|
g.appendChild(selectionOutline);
|
|
1747
1863
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1864
|
+
if (!bar.isMilestone) {
|
|
1865
|
+
const handleWidth = 6;
|
|
1866
|
+
const leftHandle = svgEl("rect");
|
|
1867
|
+
leftHandle.setAttribute("x", "0");
|
|
1868
|
+
leftHandle.setAttribute("y", "0");
|
|
1869
|
+
leftHandle.setAttribute("width", String(handleWidth));
|
|
1870
|
+
leftHandle.setAttribute("height", String(bar.height));
|
|
1871
|
+
leftHandle.setAttribute("fill", "transparent");
|
|
1872
|
+
leftHandle.dataset.ganttHandle = "left";
|
|
1873
|
+
leftHandle.dataset.ganttHandleFor = bar.taskId;
|
|
1874
|
+
leftHandle.style.cursor = "ew-resize";
|
|
1875
|
+
g.appendChild(leftHandle);
|
|
1876
|
+
const rightHandle = svgEl("rect");
|
|
1877
|
+
rightHandle.setAttribute("x", String(Math.max(0, bar.width - handleWidth)));
|
|
1878
|
+
rightHandle.setAttribute("y", "0");
|
|
1879
|
+
rightHandle.setAttribute("width", String(handleWidth));
|
|
1880
|
+
rightHandle.setAttribute("height", String(bar.height));
|
|
1881
|
+
rightHandle.setAttribute("fill", "transparent");
|
|
1882
|
+
rightHandle.dataset.ganttHandle = "right";
|
|
1883
|
+
rightHandle.dataset.ganttHandleFor = bar.taskId;
|
|
1884
|
+
rightHandle.style.cursor = "ew-resize";
|
|
1885
|
+
g.appendChild(rightHandle);
|
|
1886
|
+
}
|
|
1769
1887
|
const connector = svgEl("circle");
|
|
1770
|
-
connector.setAttribute("cx", String(bar.width));
|
|
1888
|
+
connector.setAttribute("cx", String(bar.width + (bar.isMilestone ? 10 : 0)));
|
|
1771
1889
|
connector.setAttribute("cy", String(bar.height / 2));
|
|
1772
1890
|
connector.setAttribute("r", "4");
|
|
1773
1891
|
connector.style.fill = themedFill("linkColor", model.theme.linkColor);
|
|
@@ -1775,14 +1893,16 @@ var GanttRenderer = class {
|
|
|
1775
1893
|
connector.dataset.ganttConnectorSide = "right";
|
|
1776
1894
|
connector.style.cursor = "crosshair";
|
|
1777
1895
|
g.appendChild(connector);
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1896
|
+
if (!bar.isMilestone) {
|
|
1897
|
+
const label = svgEl("text");
|
|
1898
|
+
label.setAttribute("x", String(bar.width + 6));
|
|
1899
|
+
label.setAttribute("y", String(bar.height / 2 + 4));
|
|
1900
|
+
label.style.fill = themedFill("textColor", model.theme.textColor);
|
|
1901
|
+
label.setAttribute("font-size", String(model.theme.fontSize));
|
|
1902
|
+
label.setAttribute("font-family", model.theme.fontFamily);
|
|
1903
|
+
label.textContent = bar.label;
|
|
1904
|
+
g.appendChild(label);
|
|
1905
|
+
}
|
|
1786
1906
|
if (bar.deadlineX !== void 0) {
|
|
1787
1907
|
const markerX = bar.deadlineX - bar.x;
|
|
1788
1908
|
const flag = svgEl("g");
|
|
@@ -1840,10 +1960,20 @@ var GanttRenderer = class {
|
|
|
1840
1960
|
for (const link of model.links) {
|
|
1841
1961
|
const fromBar = barByTaskId.get(link.fromId);
|
|
1842
1962
|
const toBar = barByTaskId.get(link.toId);
|
|
1843
|
-
|
|
1963
|
+
if (!fromBar || !toBar) continue;
|
|
1964
|
+
const relevantY = toBar.y ?? fromBar.y ?? 0;
|
|
1844
1965
|
if (relevantY < startY || relevantY > endY) continue;
|
|
1966
|
+
const type = link.type ?? "FS";
|
|
1967
|
+
const x1 = type.startsWith("F") ? fromBar.x + fromBar.width : fromBar.x;
|
|
1968
|
+
const y1 = fromBar.y + fromBar.height / 2;
|
|
1969
|
+
const x2 = type.endsWith("F") ? toBar.x + toBar.width : toBar.x;
|
|
1970
|
+
const y2 = toBar.y + toBar.height / 2;
|
|
1971
|
+
const offset = 10;
|
|
1972
|
+
const routingX1 = type.startsWith("F") ? x1 + offset : x1 - offset;
|
|
1973
|
+
const routingX2 = type.endsWith("F") ? x2 + offset : x2 - offset;
|
|
1974
|
+
const d = `M ${x1} ${y1} L ${routingX1} ${y1} C ${routingX1 + (routingX2 - routingX1) / 2} ${y1}, ${routingX1 + (routingX2 - routingX1) / 2} ${y2}, ${routingX2} ${y2} L ${x2} ${y2}`;
|
|
1845
1975
|
const path = svgEl("path");
|
|
1846
|
-
path.setAttribute("d",
|
|
1976
|
+
path.setAttribute("d", d);
|
|
1847
1977
|
path.setAttribute("fill", "none");
|
|
1848
1978
|
path.style.stroke = link.isCritical ? themedFill("criticalColor", model.theme.criticalColor) : themedFill("linkColor", model.theme.linkColor);
|
|
1849
1979
|
path.setAttribute("stroke-width", link.isCritical ? "2" : "1.5");
|
|
@@ -2241,6 +2371,12 @@ var InteractionController = class {
|
|
|
2241
2371
|
const toggleId = target.closest("[data-gantt-toggle]")?.dataset.ganttToggle;
|
|
2242
2372
|
if (toggleId) {
|
|
2243
2373
|
this.callbacks.onToggleCollapse(toggleId);
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const addChildId = target.closest("[data-gantt-add-child]")?.dataset.ganttAddChild;
|
|
2377
|
+
if (addChildId && this.callbacks.onAddChild) {
|
|
2378
|
+
this.callbacks.onAddChild(addChildId);
|
|
2379
|
+
return;
|
|
2244
2380
|
}
|
|
2245
2381
|
};
|
|
2246
2382
|
this.onContextMenu = (evt) => {
|
|
@@ -2700,6 +2836,7 @@ var _GanttChart = class _GanttChart {
|
|
|
2700
2836
|
markers: options.markers,
|
|
2701
2837
|
pagination: options.pagination,
|
|
2702
2838
|
snapToUnit: options.snapToUnit,
|
|
2839
|
+
headerPosition: options.headerPosition ?? "top",
|
|
2703
2840
|
onDateChange: options.onDateChange,
|
|
2704
2841
|
onProgressChange: options.onProgressChange,
|
|
2705
2842
|
onDependencyCreate: options.onDependencyCreate,
|
|
@@ -2732,6 +2869,7 @@ var _GanttChart = class _GanttChart {
|
|
|
2732
2869
|
this.darkMediaQuery.addEventListener?.("change", this.handleSchemeChange);
|
|
2733
2870
|
}
|
|
2734
2871
|
this.renderer = new GanttRenderer(this.container, {
|
|
2872
|
+
readonly: this.options.readonly,
|
|
2735
2873
|
keyboardAccessible: this.options.keyboardAccessible,
|
|
2736
2874
|
showAssigneeAvatars: this.options.showAssigneeAvatars,
|
|
2737
2875
|
virtualScroll: this.options.virtualScroll,
|
|
@@ -2741,6 +2879,13 @@ var _GanttChart = class _GanttChart {
|
|
|
2741
2879
|
onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
|
|
2742
2880
|
onSortClick: (columnId) => this.handleSortClick(columnId)
|
|
2743
2881
|
});
|
|
2882
|
+
if (this.options.headerPosition && this.renderer?.root) {
|
|
2883
|
+
this.renderer.root.dataset.ganttHeaderPosition = this.options.headerPosition;
|
|
2884
|
+
}
|
|
2885
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
2886
|
+
this.resizeObserver = new ResizeObserver(() => this.scheduleRender());
|
|
2887
|
+
this.resizeObserver.observe(this.renderer.timelineScroll);
|
|
2888
|
+
}
|
|
2744
2889
|
this.interactions = new InteractionController(
|
|
2745
2890
|
this.renderer.svg,
|
|
2746
2891
|
this.renderer.gridPanel,
|
|
@@ -2779,7 +2924,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2779
2924
|
} : void 0,
|
|
2780
2925
|
onSelectAll: this.options.selectable ? () => this.selectAllVisible() : void 0,
|
|
2781
2926
|
onJumpToStart: () => this.scrollToRangeStart(),
|
|
2782
|
-
onJumpToEnd: () => this.scrollToRangeEnd()
|
|
2927
|
+
onJumpToEnd: () => this.scrollToRangeEnd(),
|
|
2928
|
+
onAddChild: (parentId) => this.handleAddChild(parentId)
|
|
2783
2929
|
},
|
|
2784
2930
|
{
|
|
2785
2931
|
readonly: this.options.readonly,
|
|
@@ -3069,6 +3215,35 @@ var _GanttChart = class _GanttChart {
|
|
|
3069
3215
|
};
|
|
3070
3216
|
this.runCommand(cmd);
|
|
3071
3217
|
}
|
|
3218
|
+
handleAddChild(parentId) {
|
|
3219
|
+
const parent = this.tasks.find((t) => t.id === parentId);
|
|
3220
|
+
if (!parent) return;
|
|
3221
|
+
const newTask = {
|
|
3222
|
+
id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
3223
|
+
name: "New task",
|
|
3224
|
+
start: new Date(parent.start.getTime()),
|
|
3225
|
+
end: new Date(parent.start.getTime() + MS_PER_DAY),
|
|
3226
|
+
parentId: parent.id
|
|
3227
|
+
};
|
|
3228
|
+
const cmd = {
|
|
3229
|
+
label: "task-create",
|
|
3230
|
+
do: () => {
|
|
3231
|
+
const index = this.tasks.findIndex((t) => t.id === parentId);
|
|
3232
|
+
if (index >= 0) this.tasks.splice(index + 1, 0, newTask);
|
|
3233
|
+
else this.tasks.push(newTask);
|
|
3234
|
+
this.emitter.emit("task-create", { task: newTask });
|
|
3235
|
+
this.options.onTaskCreate?.(newTask);
|
|
3236
|
+
this.emitter.emit("tasks-change", { tasks: this.getTasks() });
|
|
3237
|
+
this.scheduleRender();
|
|
3238
|
+
},
|
|
3239
|
+
undo: () => {
|
|
3240
|
+
this.tasks = this.tasks.filter((t) => t.id !== newTask.id);
|
|
3241
|
+
this.emitter.emit("tasks-change", { tasks: this.getTasks() });
|
|
3242
|
+
this.scheduleRender();
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
this.runCommand(cmd);
|
|
3246
|
+
}
|
|
3072
3247
|
handleBarClick(taskId, modifiers) {
|
|
3073
3248
|
const task = this.tasks.find((t) => t.id === taskId);
|
|
3074
3249
|
if (!task) return;
|
|
@@ -3300,6 +3475,12 @@ var _GanttChart = class _GanttChart {
|
|
|
3300
3475
|
if (partial.gridPanelWidth !== void 0) {
|
|
3301
3476
|
this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
|
|
3302
3477
|
}
|
|
3478
|
+
if ("headerPosition" in partial) {
|
|
3479
|
+
this.options.headerPosition = partial.headerPosition ?? "top";
|
|
3480
|
+
if (this.renderer?.root) {
|
|
3481
|
+
this.renderer.root.dataset.ganttHeaderPosition = this.options.headerPosition;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3303
3484
|
Object.assign(this.options, partial);
|
|
3304
3485
|
this.scheduleRender();
|
|
3305
3486
|
}
|
|
@@ -3387,9 +3568,11 @@ var _GanttChart = class _GanttChart {
|
|
|
3387
3568
|
}
|
|
3388
3569
|
computeModel() {
|
|
3389
3570
|
const pagination = this.options.pagination ? { pageSize: this.options.pagination.pageSize, page: this.currentPage } : void 0;
|
|
3571
|
+
const timelineViewportWidth = HAS_DOM && this.renderer ? this.renderer.timelineScroll.clientWidth : void 0;
|
|
3390
3572
|
return computeLayout({
|
|
3391
3573
|
tasks: this.tasks,
|
|
3392
3574
|
dependencies: this.dependencies,
|
|
3575
|
+
timelineViewportWidth,
|
|
3393
3576
|
viewMode: this.options.viewMode,
|
|
3394
3577
|
columnWidth: this.columnWidth,
|
|
3395
3578
|
theme: this.theme,
|
|
@@ -3472,7 +3655,35 @@ var _GanttChart = class _GanttChart {
|
|
|
3472
3655
|
return this.renderer?.toSVGString() ?? "";
|
|
3473
3656
|
}
|
|
3474
3657
|
async rasterizeSVG(svgString, width, height) {
|
|
3475
|
-
|
|
3658
|
+
let cleanSvgString = svgString;
|
|
3659
|
+
if (typeof DOMParser !== "undefined") {
|
|
3660
|
+
const parser = new DOMParser();
|
|
3661
|
+
const doc = parser.parseFromString(svgString, "image/svg+xml");
|
|
3662
|
+
const foreignObjects = Array.from(doc.querySelectorAll("foreignObject"));
|
|
3663
|
+
for (const fo of foreignObjects) {
|
|
3664
|
+
const x = fo.getAttribute("x") || "0";
|
|
3665
|
+
const y = fo.getAttribute("y") || "0";
|
|
3666
|
+
const w = parseFloat(fo.getAttribute("width") || "0");
|
|
3667
|
+
const h = parseFloat(fo.getAttribute("height") || "0");
|
|
3668
|
+
const div = fo.querySelector("div");
|
|
3669
|
+
if (div) {
|
|
3670
|
+
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
3671
|
+
text.setAttribute("x", String(parseFloat(x) + w / 2));
|
|
3672
|
+
text.setAttribute("y", String(parseFloat(y) + h / 2 + 4));
|
|
3673
|
+
text.setAttribute("text-anchor", "middle");
|
|
3674
|
+
text.setAttribute("fill", div.style.color || "#000");
|
|
3675
|
+
text.setAttribute("font-size", div.style.fontSize || "12px");
|
|
3676
|
+
text.setAttribute("font-family", div.style.fontFamily || "sans-serif");
|
|
3677
|
+
text.setAttribute("font-weight", div.style.fontWeight || "normal");
|
|
3678
|
+
text.textContent = div.textContent;
|
|
3679
|
+
fo.parentNode?.replaceChild(text, fo);
|
|
3680
|
+
} else {
|
|
3681
|
+
fo.parentNode?.removeChild(fo);
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
cleanSvgString = new XMLSerializer().serializeToString(doc);
|
|
3685
|
+
}
|
|
3686
|
+
const svgBlob = new Blob([cleanSvgString], { type: "image/svg+xml;charset=utf-8" });
|
|
3476
3687
|
const url = URL.createObjectURL(svgBlob);
|
|
3477
3688
|
try {
|
|
3478
3689
|
return await new Promise((resolve, reject) => {
|