@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.js
CHANGED
|
@@ -25,7 +25,8 @@ var DEFAULT_THEME = {
|
|
|
25
25
|
backgroundColor: "#ffffff",
|
|
26
26
|
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
27
27
|
fontSize: 12,
|
|
28
|
-
borderRadius: 4
|
|
28
|
+
borderRadius: 4,
|
|
29
|
+
borderColor: "#e2e8f0"
|
|
29
30
|
};
|
|
30
31
|
var DARK_THEME = {
|
|
31
32
|
...DEFAULT_THEME,
|
|
@@ -42,7 +43,8 @@ var DARK_THEME = {
|
|
|
42
43
|
markerColor: "#a78bfa",
|
|
43
44
|
selectionColor: "#38bdf8",
|
|
44
45
|
textColor: "#e5e7eb",
|
|
45
|
-
backgroundColor: "#181c24"
|
|
46
|
+
backgroundColor: "#181c24",
|
|
47
|
+
borderColor: "#334155"
|
|
46
48
|
};
|
|
47
49
|
var DENSITY_PRESETS = {
|
|
48
50
|
compact: { rowHeight: 26, barHeight: 16, headerHeight: 38, fontSize: 11 },
|
|
@@ -81,7 +83,8 @@ var CSS_VAR_NAMES = {
|
|
|
81
83
|
backgroundColor: "--gantt-background-color",
|
|
82
84
|
fontFamily: "--gantt-font-family",
|
|
83
85
|
fontSize: "--gantt-font-size",
|
|
84
|
-
borderRadius: "--gantt-border-radius"
|
|
86
|
+
borderRadius: "--gantt-border-radius",
|
|
87
|
+
borderColor: "--gantt-border-color"
|
|
85
88
|
};
|
|
86
89
|
var PX_FIELDS = /* @__PURE__ */ new Set([
|
|
87
90
|
"rowHeight",
|
|
@@ -507,20 +510,38 @@ function labelFor(date, viewMode) {
|
|
|
507
510
|
const h = date.getHours();
|
|
508
511
|
const period = h >= 12 ? "PM" : "AM";
|
|
509
512
|
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
510
|
-
return
|
|
513
|
+
return {
|
|
514
|
+
label: `${h12} ${period}`,
|
|
515
|
+
parentLabel: `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()} ${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
|
|
516
|
+
};
|
|
511
517
|
}
|
|
512
518
|
case "day":
|
|
513
|
-
return
|
|
519
|
+
return {
|
|
520
|
+
label: `${WEEKDAY_LABELS[date.getDay()]} ${date.getDate()}`,
|
|
521
|
+
parentLabel: `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
|
|
522
|
+
};
|
|
514
523
|
case "week":
|
|
515
|
-
return
|
|
524
|
+
return {
|
|
525
|
+
label: `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`,
|
|
526
|
+
parentLabel: `${MONTH_LABELS[date.getMonth()]} ${date.getFullYear()}`
|
|
527
|
+
};
|
|
516
528
|
case "month":
|
|
517
|
-
return
|
|
529
|
+
return {
|
|
530
|
+
label: MONTH_LABELS[date.getMonth()] ?? "",
|
|
531
|
+
parentLabel: `${date.getFullYear()}`
|
|
532
|
+
};
|
|
518
533
|
case "quarter": {
|
|
519
534
|
const q = Math.floor(date.getMonth() / 3) + 1;
|
|
520
|
-
return
|
|
535
|
+
return {
|
|
536
|
+
label: `Q${q}`,
|
|
537
|
+
parentLabel: `${date.getFullYear()}`
|
|
538
|
+
};
|
|
521
539
|
}
|
|
522
540
|
case "year":
|
|
523
|
-
return
|
|
541
|
+
return {
|
|
542
|
+
label: `${date.getFullYear()}`,
|
|
543
|
+
parentLabel: ""
|
|
544
|
+
};
|
|
524
545
|
}
|
|
525
546
|
}
|
|
526
547
|
function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @__PURE__ */ new Date(), calendar) {
|
|
@@ -534,10 +555,12 @@ function generateTicks(rangeStart, rangeEnd, viewMode, columnWidth, today = /* @
|
|
|
534
555
|
while (cursor.getTime() < rangeEnd.getTime() && guard < MAX_TICKS) {
|
|
535
556
|
guard++;
|
|
536
557
|
const x = (cursor.getTime() - rangeStart.getTime()) * scale;
|
|
558
|
+
const labels = labelFor(cursor, viewMode);
|
|
537
559
|
ticks.push({
|
|
538
560
|
date: new Date(cursor.getTime()),
|
|
539
561
|
x,
|
|
540
|
-
label:
|
|
562
|
+
label: labels.label,
|
|
563
|
+
parentLabel: labels.parentLabel,
|
|
541
564
|
isWeekend: dayLevel ? isWeekend(cursor) : false,
|
|
542
565
|
isNonWorking: viewMode === "hour" && calendar?.workingHours ? !isWorkingTime(cursor, calendar) : dayLevel ? isNonWorkingDay(cursor, calendar) : false,
|
|
543
566
|
isToday: isSameDay(cursor, today) && dayLevel,
|
|
@@ -778,11 +801,14 @@ function computeLayout(input) {
|
|
|
778
801
|
let paddedStart = addUnit(rawRangeStart, viewMode, -1);
|
|
779
802
|
let paddedEnd = addUnit(rawRangeEnd, viewMode, 1);
|
|
780
803
|
const minUnits = MIN_VISIBLE_UNITS[viewMode];
|
|
804
|
+
let targetMinUnits = minUnits;
|
|
805
|
+
if (input.timelineViewportWidth && input.timelineViewportWidth > 0) {
|
|
806
|
+
targetMinUnits = Math.max(targetMinUnits, Math.ceil(input.timelineViewportWidth / columnWidth));
|
|
807
|
+
}
|
|
781
808
|
const currentUnits = (paddedEnd.getTime() - paddedStart.getTime()) / approxUnitMs(viewMode);
|
|
782
|
-
if (currentUnits <
|
|
783
|
-
const deficit =
|
|
784
|
-
|
|
785
|
-
paddedEnd = addUnit(paddedEnd, viewMode, Math.floor(deficit / 2));
|
|
809
|
+
if (currentUnits < targetMinUnits) {
|
|
810
|
+
const deficit = targetMinUnits - currentUnits;
|
|
811
|
+
paddedEnd = addUnit(paddedEnd, viewMode, Math.ceil(deficit));
|
|
786
812
|
}
|
|
787
813
|
const rows = [];
|
|
788
814
|
const bars = [];
|
|
@@ -900,6 +926,7 @@ function computeLayout(input) {
|
|
|
900
926
|
(t) => ({
|
|
901
927
|
x: t.x,
|
|
902
928
|
label: t.label,
|
|
929
|
+
parentLabel: t.parentLabel,
|
|
903
930
|
isWeekend: t.isWeekend,
|
|
904
931
|
isNonWorking: t.isNonWorking,
|
|
905
932
|
isToday: t.isToday,
|
|
@@ -1177,6 +1204,9 @@ var GanttRenderer = class {
|
|
|
1177
1204
|
this.svg = svgEl("svg");
|
|
1178
1205
|
this.svg.classList.add("gantt-svg");
|
|
1179
1206
|
this.timelineScroll.appendChild(this.headerContainer);
|
|
1207
|
+
if (this.options.readonly) {
|
|
1208
|
+
this.root.classList.add("gantt-readonly");
|
|
1209
|
+
}
|
|
1180
1210
|
this.timelineScroll.appendChild(this.svg);
|
|
1181
1211
|
this.root.appendChild(this.gridPanel);
|
|
1182
1212
|
this.root.appendChild(this.timelineScroll);
|
|
@@ -1244,7 +1274,10 @@ var GanttRenderer = class {
|
|
|
1244
1274
|
const cell = el("div", "gantt-grid-header-cell");
|
|
1245
1275
|
cell.style.position = "relative";
|
|
1246
1276
|
if (col.width) cell.style.width = `${col.width}px`;
|
|
1247
|
-
|
|
1277
|
+
const titleSpan = document.createElement("span");
|
|
1278
|
+
titleSpan.className = "gantt-cell-text";
|
|
1279
|
+
titleSpan.textContent = col.title;
|
|
1280
|
+
cell.appendChild(titleSpan);
|
|
1248
1281
|
if (col.sortable) {
|
|
1249
1282
|
cell.classList.add("gantt-grid-header-cell-sortable");
|
|
1250
1283
|
const active = sort?.columnId === col.id;
|
|
@@ -1286,7 +1319,7 @@ var GanttRenderer = class {
|
|
|
1286
1319
|
evt.preventDefault();
|
|
1287
1320
|
evt.stopPropagation();
|
|
1288
1321
|
const startX = evt.clientX;
|
|
1289
|
-
const startWidth =
|
|
1322
|
+
const startWidth = cell.getBoundingClientRect().width;
|
|
1290
1323
|
const onMove = (moveEvt) => {
|
|
1291
1324
|
const newWidth = Math.max(30, startWidth + (moveEvt.clientX - startX));
|
|
1292
1325
|
cell.style.width = `${newWidth}px`;
|
|
@@ -1339,9 +1372,11 @@ var GanttRenderer = class {
|
|
|
1339
1372
|
const cell = el("div", "gantt-grid-cell");
|
|
1340
1373
|
if (col.width) cell.style.width = `${col.width}px`;
|
|
1341
1374
|
if (col.align) cell.style.textAlign = col.align;
|
|
1342
|
-
const
|
|
1375
|
+
const isTreeColumn = col.id === "name" || !columns.some((c) => c.id === "name") && colIndex === 0;
|
|
1376
|
+
console.log(`Column ${col.id}: isTreeColumn=${isTreeColumn}, colIndex=${colIndex}, hasNameCol=${columns.some((c) => c.id === "name")}`);
|
|
1377
|
+
const rawValue = col.accessor ? col.accessor(task) : isTreeColumn ? task.name : task[col.id] ?? "";
|
|
1343
1378
|
const textValue = rawValue === null || rawValue === void 0 ? "" : String(rawValue);
|
|
1344
|
-
if (
|
|
1379
|
+
if (isTreeColumn) {
|
|
1345
1380
|
cell.dataset.ganttNameCell = task.id;
|
|
1346
1381
|
cell.dataset.ganttNameValue = textValue;
|
|
1347
1382
|
const indent = el("span", "gantt-indent");
|
|
@@ -1350,17 +1385,27 @@ var GanttRenderer = class {
|
|
|
1350
1385
|
if (row.hasChildren) {
|
|
1351
1386
|
const toggle = el("button", "gantt-toggle");
|
|
1352
1387
|
toggle.type = "button";
|
|
1388
|
+
toggle.className = `gantt-toggle ${row.collapsed ? "collapsed" : "expanded"}`;
|
|
1353
1389
|
toggle.dataset.ganttToggle = task.id;
|
|
1354
1390
|
toggle.setAttribute("aria-label", row.collapsed ? "Expand" : "Collapse");
|
|
1355
1391
|
toggle.textContent = row.collapsed ? "\u25B8" : "\u25BE";
|
|
1356
1392
|
cell.appendChild(toggle);
|
|
1357
1393
|
}
|
|
1394
|
+
const addChildBtn = el("button", "gantt-add-child");
|
|
1395
|
+
addChildBtn.type = "button";
|
|
1396
|
+
addChildBtn.dataset.ganttAddChild = task.id;
|
|
1397
|
+
addChildBtn.setAttribute("aria-label", "Add child task");
|
|
1398
|
+
addChildBtn.textContent = "+";
|
|
1399
|
+
cell.appendChild(addChildBtn);
|
|
1358
1400
|
}
|
|
1359
1401
|
const rendered = col.render ? col.render(task) : void 0;
|
|
1360
1402
|
if (rendered instanceof HTMLElement) {
|
|
1361
1403
|
cell.appendChild(rendered);
|
|
1362
1404
|
} else if (typeof rendered === "string") {
|
|
1363
|
-
|
|
1405
|
+
const span = document.createElement("span");
|
|
1406
|
+
span.className = "gantt-cell-text";
|
|
1407
|
+
span.textContent = rendered;
|
|
1408
|
+
cell.appendChild(span);
|
|
1364
1409
|
} else {
|
|
1365
1410
|
const href = col.getHref ? col.getHref(task) : null;
|
|
1366
1411
|
if (href && isSafeHref(href)) {
|
|
@@ -1369,10 +1414,13 @@ var GanttRenderer = class {
|
|
|
1369
1414
|
anchor.target = col.linkTarget ?? "_blank";
|
|
1370
1415
|
anchor.rel = "noopener noreferrer";
|
|
1371
1416
|
anchor.textContent = textValue;
|
|
1417
|
+
anchor.className = "gantt-cell-text";
|
|
1372
1418
|
cell.appendChild(anchor);
|
|
1373
1419
|
} else {
|
|
1374
|
-
const
|
|
1375
|
-
cell
|
|
1420
|
+
const span = document.createElement("span");
|
|
1421
|
+
span.className = "gantt-cell-text";
|
|
1422
|
+
span.textContent = textValue;
|
|
1423
|
+
cell.appendChild(span);
|
|
1376
1424
|
}
|
|
1377
1425
|
}
|
|
1378
1426
|
rowEl.appendChild(cell);
|
|
@@ -1403,8 +1451,8 @@ var GanttRenderer = class {
|
|
|
1403
1451
|
const bodyGroup = svgEl("g");
|
|
1404
1452
|
this.renderGridLines(bodyGroup, model);
|
|
1405
1453
|
this.renderRowBackgrounds(bodyGroup, model, startY, endY);
|
|
1406
|
-
this.renderBars(bodyGroup, model, tasks, startY, endY);
|
|
1407
1454
|
this.renderLinks(bodyGroup, model, startY, endY);
|
|
1455
|
+
this.renderBars(bodyGroup, model, tasks, startY, endY);
|
|
1408
1456
|
this.renderMarkers(bodyGroup, model);
|
|
1409
1457
|
this.svg.appendChild(bodyGroup);
|
|
1410
1458
|
this.renderHeader(model);
|
|
@@ -1471,15 +1519,21 @@ var GanttRenderer = class {
|
|
|
1471
1519
|
clipRect.setAttribute("height", String(model.height));
|
|
1472
1520
|
clipPath.appendChild(clipRect);
|
|
1473
1521
|
defs.appendChild(clipPath);
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1522
|
+
const fo = svgEl("foreignObject");
|
|
1523
|
+
fo.setAttribute("x", String(marker.x + 4));
|
|
1524
|
+
fo.setAttribute("y", "0");
|
|
1525
|
+
fo.setAttribute("width", String(cellWidth > 8 ? cellWidth - 8 : cellWidth));
|
|
1526
|
+
fo.setAttribute("height", String(model.height));
|
|
1527
|
+
const div = document.createElement("div");
|
|
1528
|
+
div.style.color = marker.color;
|
|
1529
|
+
div.style.fontSize = `${model.theme.fontSize}px`;
|
|
1530
|
+
div.style.fontFamily = model.theme.fontFamily;
|
|
1531
|
+
div.style.whiteSpace = "normal";
|
|
1532
|
+
div.style.overflowWrap = "break-word";
|
|
1533
|
+
div.style.paddingTop = "4px";
|
|
1534
|
+
div.textContent = marker.label;
|
|
1535
|
+
fo.appendChild(div);
|
|
1536
|
+
group.appendChild(fo);
|
|
1483
1537
|
}
|
|
1484
1538
|
});
|
|
1485
1539
|
}
|
|
@@ -1500,6 +1554,60 @@ var GanttRenderer = class {
|
|
|
1500
1554
|
headerGroup.appendChild(bg);
|
|
1501
1555
|
const defs = svgEl("defs");
|
|
1502
1556
|
const ticks = model.ticks;
|
|
1557
|
+
const parentGroups = [];
|
|
1558
|
+
let currentGroup = null;
|
|
1559
|
+
for (const tick of ticks) {
|
|
1560
|
+
if (!currentGroup || currentGroup.label !== tick.parentLabel) {
|
|
1561
|
+
currentGroup = { x: tick.x, label: tick.parentLabel };
|
|
1562
|
+
parentGroups.push(currentGroup);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const hasParentTier = parentGroups.some((g) => g.label !== "");
|
|
1566
|
+
const bottomTierY = hasParentTier ? model.headerHeight / 2 : 0;
|
|
1567
|
+
const tierHeight = hasParentTier ? model.headerHeight / 2 : model.headerHeight;
|
|
1568
|
+
if (hasParentTier) {
|
|
1569
|
+
for (let i = 0; i < parentGroups.length; i++) {
|
|
1570
|
+
const group = parentGroups[i];
|
|
1571
|
+
const nextX = parentGroups[i + 1]?.x ?? model.width;
|
|
1572
|
+
const cellWidth = nextX - group.x;
|
|
1573
|
+
const fo = svgEl("foreignObject");
|
|
1574
|
+
fo.setAttribute("x", String(group.x + 4));
|
|
1575
|
+
fo.setAttribute("y", "0");
|
|
1576
|
+
fo.setAttribute("width", String(Math.max(0, cellWidth - 4)));
|
|
1577
|
+
fo.setAttribute("height", String(tierHeight));
|
|
1578
|
+
const div = document.createElement("div");
|
|
1579
|
+
div.style.color = themedFill("textColor", model.theme.textColor);
|
|
1580
|
+
div.style.fontSize = `${model.theme.fontSize * 0.95}px`;
|
|
1581
|
+
div.style.fontFamily = model.theme.fontFamily;
|
|
1582
|
+
div.style.fontWeight = "600";
|
|
1583
|
+
div.style.whiteSpace = "nowrap";
|
|
1584
|
+
div.style.overflow = "hidden";
|
|
1585
|
+
div.style.textOverflow = "ellipsis";
|
|
1586
|
+
div.style.display = "flex";
|
|
1587
|
+
div.style.alignItems = "center";
|
|
1588
|
+
div.style.justifyContent = "center";
|
|
1589
|
+
div.style.height = "100%";
|
|
1590
|
+
div.textContent = group.label;
|
|
1591
|
+
fo.appendChild(div);
|
|
1592
|
+
headerGroup.appendChild(fo);
|
|
1593
|
+
const border = svgEl("line");
|
|
1594
|
+
border.setAttribute("x1", String(group.x));
|
|
1595
|
+
border.setAttribute("x2", String(nextX));
|
|
1596
|
+
border.setAttribute("y1", String(tierHeight));
|
|
1597
|
+
border.setAttribute("y2", String(tierHeight));
|
|
1598
|
+
border.style.stroke = themedFill("borderColor", model.theme.borderColor ?? "#e2e8f0");
|
|
1599
|
+
headerGroup.appendChild(border);
|
|
1600
|
+
if (i > 0) {
|
|
1601
|
+
const sep = svgEl("line");
|
|
1602
|
+
sep.setAttribute("x1", String(group.x));
|
|
1603
|
+
sep.setAttribute("x2", String(group.x));
|
|
1604
|
+
sep.setAttribute("y1", "0");
|
|
1605
|
+
sep.setAttribute("y2", String(tierHeight));
|
|
1606
|
+
sep.style.stroke = themedFill("borderColor", model.theme.borderColor ?? "#e2e8f0");
|
|
1607
|
+
headerGroup.appendChild(sep);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1503
1611
|
for (let i = 0; i < ticks.length; i++) {
|
|
1504
1612
|
const tick = ticks[i];
|
|
1505
1613
|
const cellWidth = (ticks[i + 1]?.x ?? model.width) - tick.x;
|
|
@@ -1508,20 +1616,30 @@ var GanttRenderer = class {
|
|
|
1508
1616
|
clipPath.setAttribute("id", clipId);
|
|
1509
1617
|
const clipRect = svgEl("rect");
|
|
1510
1618
|
clipRect.setAttribute("x", String(tick.x));
|
|
1511
|
-
clipRect.setAttribute("y",
|
|
1619
|
+
clipRect.setAttribute("y", String(bottomTierY));
|
|
1512
1620
|
clipRect.setAttribute("width", String(Math.max(0, cellWidth)));
|
|
1513
|
-
clipRect.setAttribute("height", String(
|
|
1621
|
+
clipRect.setAttribute("height", String(tierHeight));
|
|
1514
1622
|
clipPath.appendChild(clipRect);
|
|
1515
1623
|
defs.appendChild(clipPath);
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1624
|
+
const fo = svgEl("foreignObject");
|
|
1625
|
+
fo.setAttribute("x", String(tick.x + 4));
|
|
1626
|
+
fo.setAttribute("y", String(bottomTierY));
|
|
1627
|
+
fo.setAttribute("width", String(cellWidth > 8 ? cellWidth - 8 : cellWidth));
|
|
1628
|
+
fo.setAttribute("height", String(tierHeight));
|
|
1629
|
+
const div = document.createElement("div");
|
|
1630
|
+
div.style.color = themedFill("textColor", model.theme.textColor);
|
|
1631
|
+
div.style.fontSize = `${model.theme.fontSize}px`;
|
|
1632
|
+
div.style.fontFamily = model.theme.fontFamily;
|
|
1633
|
+
div.style.whiteSpace = "normal";
|
|
1634
|
+
div.style.overflowWrap = "break-word";
|
|
1635
|
+
div.style.display = "flex";
|
|
1636
|
+
div.style.alignItems = hasParentTier ? "center" : "flex-end";
|
|
1637
|
+
div.style.justifyContent = "center";
|
|
1638
|
+
div.style.paddingBottom = hasParentTier ? "0" : "4px";
|
|
1639
|
+
div.style.height = "100%";
|
|
1640
|
+
div.textContent = tick.label;
|
|
1641
|
+
fo.appendChild(div);
|
|
1642
|
+
headerGroup.appendChild(fo);
|
|
1525
1643
|
}
|
|
1526
1644
|
headerGroup.insertBefore(defs, headerGroup.firstChild);
|
|
1527
1645
|
this.headerSvg.appendChild(headerGroup);
|
|
@@ -1564,8 +1682,8 @@ var GanttRenderer = class {
|
|
|
1564
1682
|
g.setAttribute("role", "button");
|
|
1565
1683
|
if (task) {
|
|
1566
1684
|
const progress = Math.round(task.progress ?? 0);
|
|
1567
|
-
const
|
|
1568
|
-
g.setAttribute("aria-label",
|
|
1685
|
+
const label = `${task.name}, ${task.start.toDateString()} to ${task.end.toDateString()}, ${progress}% complete`;
|
|
1686
|
+
g.setAttribute("aria-label", label);
|
|
1569
1687
|
}
|
|
1570
1688
|
}
|
|
1571
1689
|
if (bar.baseline) {
|
|
@@ -1613,17 +1731,15 @@ var GanttRenderer = class {
|
|
|
1613
1731
|
`rotate(45, 0, ${bar.height / 2})`
|
|
1614
1732
|
);
|
|
1615
1733
|
g.appendChild(diamond);
|
|
1616
|
-
const
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
g.appendChild(
|
|
1624
|
-
|
|
1625
|
-
}
|
|
1626
|
-
if (bar.segments && bar.segments.length > 0) {
|
|
1734
|
+
const label = svgEl("text");
|
|
1735
|
+
label.setAttribute("x", String(size / 2 + 8));
|
|
1736
|
+
label.setAttribute("y", String(bar.height / 2 + 4));
|
|
1737
|
+
label.style.fill = themedFill("textColor", model.theme.textColor);
|
|
1738
|
+
label.setAttribute("font-size", String(model.theme.fontSize));
|
|
1739
|
+
label.setAttribute("font-family", model.theme.fontFamily);
|
|
1740
|
+
label.textContent = bar.label;
|
|
1741
|
+
g.appendChild(label);
|
|
1742
|
+
} else if (bar.segments && bar.segments.length > 0) {
|
|
1627
1743
|
bar.segments.forEach((seg, i) => {
|
|
1628
1744
|
const segRect = svgEl("rect");
|
|
1629
1745
|
segRect.setAttribute("x", String(seg.x));
|
|
@@ -1696,29 +1812,31 @@ var GanttRenderer = class {
|
|
|
1696
1812
|
selectionOutline.setAttribute("stroke-width", "2");
|
|
1697
1813
|
g.appendChild(selectionOutline);
|
|
1698
1814
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1815
|
+
if (!bar.isMilestone) {
|
|
1816
|
+
const handleWidth = 6;
|
|
1817
|
+
const leftHandle = svgEl("rect");
|
|
1818
|
+
leftHandle.setAttribute("x", "0");
|
|
1819
|
+
leftHandle.setAttribute("y", "0");
|
|
1820
|
+
leftHandle.setAttribute("width", String(handleWidth));
|
|
1821
|
+
leftHandle.setAttribute("height", String(bar.height));
|
|
1822
|
+
leftHandle.setAttribute("fill", "transparent");
|
|
1823
|
+
leftHandle.dataset.ganttHandle = "left";
|
|
1824
|
+
leftHandle.dataset.ganttHandleFor = bar.taskId;
|
|
1825
|
+
leftHandle.style.cursor = "ew-resize";
|
|
1826
|
+
g.appendChild(leftHandle);
|
|
1827
|
+
const rightHandle = svgEl("rect");
|
|
1828
|
+
rightHandle.setAttribute("x", String(Math.max(0, bar.width - handleWidth)));
|
|
1829
|
+
rightHandle.setAttribute("y", "0");
|
|
1830
|
+
rightHandle.setAttribute("width", String(handleWidth));
|
|
1831
|
+
rightHandle.setAttribute("height", String(bar.height));
|
|
1832
|
+
rightHandle.setAttribute("fill", "transparent");
|
|
1833
|
+
rightHandle.dataset.ganttHandle = "right";
|
|
1834
|
+
rightHandle.dataset.ganttHandleFor = bar.taskId;
|
|
1835
|
+
rightHandle.style.cursor = "ew-resize";
|
|
1836
|
+
g.appendChild(rightHandle);
|
|
1837
|
+
}
|
|
1720
1838
|
const connector = svgEl("circle");
|
|
1721
|
-
connector.setAttribute("cx", String(bar.width));
|
|
1839
|
+
connector.setAttribute("cx", String(bar.width + (bar.isMilestone ? 10 : 0)));
|
|
1722
1840
|
connector.setAttribute("cy", String(bar.height / 2));
|
|
1723
1841
|
connector.setAttribute("r", "4");
|
|
1724
1842
|
connector.style.fill = themedFill("linkColor", model.theme.linkColor);
|
|
@@ -1726,14 +1844,16 @@ var GanttRenderer = class {
|
|
|
1726
1844
|
connector.dataset.ganttConnectorSide = "right";
|
|
1727
1845
|
connector.style.cursor = "crosshair";
|
|
1728
1846
|
g.appendChild(connector);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1847
|
+
if (!bar.isMilestone) {
|
|
1848
|
+
const label = svgEl("text");
|
|
1849
|
+
label.setAttribute("x", String(bar.width + 6));
|
|
1850
|
+
label.setAttribute("y", String(bar.height / 2 + 4));
|
|
1851
|
+
label.style.fill = themedFill("textColor", model.theme.textColor);
|
|
1852
|
+
label.setAttribute("font-size", String(model.theme.fontSize));
|
|
1853
|
+
label.setAttribute("font-family", model.theme.fontFamily);
|
|
1854
|
+
label.textContent = bar.label;
|
|
1855
|
+
g.appendChild(label);
|
|
1856
|
+
}
|
|
1737
1857
|
if (bar.deadlineX !== void 0) {
|
|
1738
1858
|
const markerX = bar.deadlineX - bar.x;
|
|
1739
1859
|
const flag = svgEl("g");
|
|
@@ -1791,10 +1911,20 @@ var GanttRenderer = class {
|
|
|
1791
1911
|
for (const link of model.links) {
|
|
1792
1912
|
const fromBar = barByTaskId.get(link.fromId);
|
|
1793
1913
|
const toBar = barByTaskId.get(link.toId);
|
|
1794
|
-
|
|
1914
|
+
if (!fromBar || !toBar) continue;
|
|
1915
|
+
const relevantY = toBar.y ?? fromBar.y ?? 0;
|
|
1795
1916
|
if (relevantY < startY || relevantY > endY) continue;
|
|
1917
|
+
const type = link.type ?? "FS";
|
|
1918
|
+
const x1 = type.startsWith("F") ? fromBar.x + fromBar.width : fromBar.x;
|
|
1919
|
+
const y1 = fromBar.y + fromBar.height / 2;
|
|
1920
|
+
const x2 = type.endsWith("F") ? toBar.x + toBar.width : toBar.x;
|
|
1921
|
+
const y2 = toBar.y + toBar.height / 2;
|
|
1922
|
+
const offset = 10;
|
|
1923
|
+
const routingX1 = type.startsWith("F") ? x1 + offset : x1 - offset;
|
|
1924
|
+
const routingX2 = type.endsWith("F") ? x2 + offset : x2 - offset;
|
|
1925
|
+
const d = `M ${x1} ${y1} L ${routingX1} ${y1} C ${routingX1 + (routingX2 - routingX1) / 2} ${y1}, ${routingX1 + (routingX2 - routingX1) / 2} ${y2}, ${routingX2} ${y2} L ${x2} ${y2}`;
|
|
1796
1926
|
const path = svgEl("path");
|
|
1797
|
-
path.setAttribute("d",
|
|
1927
|
+
path.setAttribute("d", d);
|
|
1798
1928
|
path.setAttribute("fill", "none");
|
|
1799
1929
|
path.style.stroke = link.isCritical ? themedFill("criticalColor", model.theme.criticalColor) : themedFill("linkColor", model.theme.linkColor);
|
|
1800
1930
|
path.setAttribute("stroke-width", link.isCritical ? "2" : "1.5");
|
|
@@ -2192,6 +2322,12 @@ var InteractionController = class {
|
|
|
2192
2322
|
const toggleId = target.closest("[data-gantt-toggle]")?.dataset.ganttToggle;
|
|
2193
2323
|
if (toggleId) {
|
|
2194
2324
|
this.callbacks.onToggleCollapse(toggleId);
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
const addChildId = target.closest("[data-gantt-add-child]")?.dataset.ganttAddChild;
|
|
2328
|
+
if (addChildId && this.callbacks.onAddChild) {
|
|
2329
|
+
this.callbacks.onAddChild(addChildId);
|
|
2330
|
+
return;
|
|
2195
2331
|
}
|
|
2196
2332
|
};
|
|
2197
2333
|
this.onContextMenu = (evt) => {
|
|
@@ -2651,6 +2787,7 @@ var _GanttChart = class _GanttChart {
|
|
|
2651
2787
|
markers: options.markers,
|
|
2652
2788
|
pagination: options.pagination,
|
|
2653
2789
|
snapToUnit: options.snapToUnit,
|
|
2790
|
+
headerPosition: options.headerPosition ?? "top",
|
|
2654
2791
|
onDateChange: options.onDateChange,
|
|
2655
2792
|
onProgressChange: options.onProgressChange,
|
|
2656
2793
|
onDependencyCreate: options.onDependencyCreate,
|
|
@@ -2683,6 +2820,7 @@ var _GanttChart = class _GanttChart {
|
|
|
2683
2820
|
this.darkMediaQuery.addEventListener?.("change", this.handleSchemeChange);
|
|
2684
2821
|
}
|
|
2685
2822
|
this.renderer = new GanttRenderer(this.container, {
|
|
2823
|
+
readonly: this.options.readonly,
|
|
2686
2824
|
keyboardAccessible: this.options.keyboardAccessible,
|
|
2687
2825
|
showAssigneeAvatars: this.options.showAssigneeAvatars,
|
|
2688
2826
|
virtualScroll: this.options.virtualScroll,
|
|
@@ -2692,6 +2830,13 @@ var _GanttChart = class _GanttChart {
|
|
|
2692
2830
|
onRowReorder: this.options.onTaskReorder ? (draggedId, targetId, position) => this.handleRowReorder(draggedId, targetId, position) : void 0,
|
|
2693
2831
|
onSortClick: (columnId) => this.handleSortClick(columnId)
|
|
2694
2832
|
});
|
|
2833
|
+
if (this.options.headerPosition && this.renderer?.root) {
|
|
2834
|
+
this.renderer.root.dataset.ganttHeaderPosition = this.options.headerPosition;
|
|
2835
|
+
}
|
|
2836
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
2837
|
+
this.resizeObserver = new ResizeObserver(() => this.scheduleRender());
|
|
2838
|
+
this.resizeObserver.observe(this.renderer.timelineScroll);
|
|
2839
|
+
}
|
|
2695
2840
|
this.interactions = new InteractionController(
|
|
2696
2841
|
this.renderer.svg,
|
|
2697
2842
|
this.renderer.gridPanel,
|
|
@@ -2730,7 +2875,8 @@ var _GanttChart = class _GanttChart {
|
|
|
2730
2875
|
} : void 0,
|
|
2731
2876
|
onSelectAll: this.options.selectable ? () => this.selectAllVisible() : void 0,
|
|
2732
2877
|
onJumpToStart: () => this.scrollToRangeStart(),
|
|
2733
|
-
onJumpToEnd: () => this.scrollToRangeEnd()
|
|
2878
|
+
onJumpToEnd: () => this.scrollToRangeEnd(),
|
|
2879
|
+
onAddChild: (parentId) => this.handleAddChild(parentId)
|
|
2734
2880
|
},
|
|
2735
2881
|
{
|
|
2736
2882
|
readonly: this.options.readonly,
|
|
@@ -3020,6 +3166,35 @@ var _GanttChart = class _GanttChart {
|
|
|
3020
3166
|
};
|
|
3021
3167
|
this.runCommand(cmd);
|
|
3022
3168
|
}
|
|
3169
|
+
handleAddChild(parentId) {
|
|
3170
|
+
const parent = this.tasks.find((t) => t.id === parentId);
|
|
3171
|
+
if (!parent) return;
|
|
3172
|
+
const newTask = {
|
|
3173
|
+
id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
3174
|
+
name: "New task",
|
|
3175
|
+
start: new Date(parent.start.getTime()),
|
|
3176
|
+
end: new Date(parent.start.getTime() + MS_PER_DAY),
|
|
3177
|
+
parentId: parent.id
|
|
3178
|
+
};
|
|
3179
|
+
const cmd = {
|
|
3180
|
+
label: "task-create",
|
|
3181
|
+
do: () => {
|
|
3182
|
+
const index = this.tasks.findIndex((t) => t.id === parentId);
|
|
3183
|
+
if (index >= 0) this.tasks.splice(index + 1, 0, newTask);
|
|
3184
|
+
else this.tasks.push(newTask);
|
|
3185
|
+
this.emitter.emit("task-create", { task: newTask });
|
|
3186
|
+
this.options.onTaskCreate?.(newTask);
|
|
3187
|
+
this.emitter.emit("tasks-change", { tasks: this.getTasks() });
|
|
3188
|
+
this.scheduleRender();
|
|
3189
|
+
},
|
|
3190
|
+
undo: () => {
|
|
3191
|
+
this.tasks = this.tasks.filter((t) => t.id !== newTask.id);
|
|
3192
|
+
this.emitter.emit("tasks-change", { tasks: this.getTasks() });
|
|
3193
|
+
this.scheduleRender();
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
3196
|
+
this.runCommand(cmd);
|
|
3197
|
+
}
|
|
3023
3198
|
handleBarClick(taskId, modifiers) {
|
|
3024
3199
|
const task = this.tasks.find((t) => t.id === taskId);
|
|
3025
3200
|
if (!task) return;
|
|
@@ -3251,6 +3426,12 @@ var _GanttChart = class _GanttChart {
|
|
|
3251
3426
|
if (partial.gridPanelWidth !== void 0) {
|
|
3252
3427
|
this.container.style.setProperty("--gantt-grid-panel-width", `${partial.gridPanelWidth}px`);
|
|
3253
3428
|
}
|
|
3429
|
+
if ("headerPosition" in partial) {
|
|
3430
|
+
this.options.headerPosition = partial.headerPosition ?? "top";
|
|
3431
|
+
if (this.renderer?.root) {
|
|
3432
|
+
this.renderer.root.dataset.ganttHeaderPosition = this.options.headerPosition;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3254
3435
|
Object.assign(this.options, partial);
|
|
3255
3436
|
this.scheduleRender();
|
|
3256
3437
|
}
|
|
@@ -3338,9 +3519,11 @@ var _GanttChart = class _GanttChart {
|
|
|
3338
3519
|
}
|
|
3339
3520
|
computeModel() {
|
|
3340
3521
|
const pagination = this.options.pagination ? { pageSize: this.options.pagination.pageSize, page: this.currentPage } : void 0;
|
|
3522
|
+
const timelineViewportWidth = HAS_DOM && this.renderer ? this.renderer.timelineScroll.clientWidth : void 0;
|
|
3341
3523
|
return computeLayout({
|
|
3342
3524
|
tasks: this.tasks,
|
|
3343
3525
|
dependencies: this.dependencies,
|
|
3526
|
+
timelineViewportWidth,
|
|
3344
3527
|
viewMode: this.options.viewMode,
|
|
3345
3528
|
columnWidth: this.columnWidth,
|
|
3346
3529
|
theme: this.theme,
|
|
@@ -3423,7 +3606,35 @@ var _GanttChart = class _GanttChart {
|
|
|
3423
3606
|
return this.renderer?.toSVGString() ?? "";
|
|
3424
3607
|
}
|
|
3425
3608
|
async rasterizeSVG(svgString, width, height) {
|
|
3426
|
-
|
|
3609
|
+
let cleanSvgString = svgString;
|
|
3610
|
+
if (typeof DOMParser !== "undefined") {
|
|
3611
|
+
const parser = new DOMParser();
|
|
3612
|
+
const doc = parser.parseFromString(svgString, "image/svg+xml");
|
|
3613
|
+
const foreignObjects = Array.from(doc.querySelectorAll("foreignObject"));
|
|
3614
|
+
for (const fo of foreignObjects) {
|
|
3615
|
+
const x = fo.getAttribute("x") || "0";
|
|
3616
|
+
const y = fo.getAttribute("y") || "0";
|
|
3617
|
+
const w = parseFloat(fo.getAttribute("width") || "0");
|
|
3618
|
+
const h = parseFloat(fo.getAttribute("height") || "0");
|
|
3619
|
+
const div = fo.querySelector("div");
|
|
3620
|
+
if (div) {
|
|
3621
|
+
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
3622
|
+
text.setAttribute("x", String(parseFloat(x) + w / 2));
|
|
3623
|
+
text.setAttribute("y", String(parseFloat(y) + h / 2 + 4));
|
|
3624
|
+
text.setAttribute("text-anchor", "middle");
|
|
3625
|
+
text.setAttribute("fill", div.style.color || "#000");
|
|
3626
|
+
text.setAttribute("font-size", div.style.fontSize || "12px");
|
|
3627
|
+
text.setAttribute("font-family", div.style.fontFamily || "sans-serif");
|
|
3628
|
+
text.setAttribute("font-weight", div.style.fontWeight || "normal");
|
|
3629
|
+
text.textContent = div.textContent;
|
|
3630
|
+
fo.parentNode?.replaceChild(text, fo);
|
|
3631
|
+
} else {
|
|
3632
|
+
fo.parentNode?.removeChild(fo);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
cleanSvgString = new XMLSerializer().serializeToString(doc);
|
|
3636
|
+
}
|
|
3637
|
+
const svgBlob = new Blob([cleanSvgString], { type: "image/svg+xml;charset=utf-8" });
|
|
3427
3638
|
const url = URL.createObjectURL(svgBlob);
|
|
3428
3639
|
try {
|
|
3429
3640
|
return await new Promise((resolve, reject) => {
|