gantt-lib 0.112.0 → 0.113.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/dist/index.js CHANGED
@@ -699,10 +699,10 @@ function buildTaskRangeFromStart(startDate, duration, businessDays = false, week
699
699
  end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
700
700
  };
701
701
  }
702
- const DAY_MS5 = 24 * 60 * 60 * 1e3;
702
+ const DAY_MS6 = 24 * 60 * 60 * 1e3;
703
703
  return {
704
704
  start: normalizedStart,
705
- end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS5)
705
+ end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS6)
706
706
  };
707
707
  }
708
708
  function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
@@ -713,9 +713,9 @@ function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendP
713
713
  end: normalizedEnd
714
714
  };
715
715
  }
716
- const DAY_MS5 = 24 * 60 * 60 * 1e3;
716
+ const DAY_MS6 = 24 * 60 * 60 * 1e3;
717
717
  return {
718
- start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS5),
718
+ start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS6),
719
719
  end: normalizedEnd
720
720
  };
721
721
  }
@@ -1130,13 +1130,13 @@ function computeParentProgress(parentId, tasks) {
1130
1130
  if (children.length === 0) {
1131
1131
  return 0;
1132
1132
  }
1133
- const DAY_MS5 = 24 * 60 * 60 * 1e3;
1133
+ const DAY_MS6 = 24 * 60 * 60 * 1e3;
1134
1134
  let totalWeight = 0;
1135
1135
  let weightedSum = 0;
1136
1136
  for (const child of children) {
1137
1137
  const start = new Date(child.startDate).getTime();
1138
1138
  const end = new Date(child.endDate).getTime();
1139
- const duration = (end - start + DAY_MS5) / DAY_MS5;
1139
+ const duration = (end - start + DAY_MS6) / DAY_MS6;
1140
1140
  const progress = child.progress ?? 0;
1141
1141
  totalWeight += duration;
1142
1142
  weightedSum += duration * progress;
@@ -10566,8 +10566,9 @@ function TableMatrix({
10566
10566
  }
10567
10567
 
10568
10568
  // src/components/PlanFactMatrix/PlanFactMatrix.tsx
10569
- var import_react17 = require("react");
10569
+ var import_react17 = __toESM(require("react"));
10570
10570
  var import_jsx_runtime18 = require("react/jsx-runtime");
10571
+ var DAY_MS5 = 24 * 60 * 60 * 1e3;
10571
10572
  function joinClasses2(...values) {
10572
10573
  return values.filter(Boolean).join(" ");
10573
10574
  }
@@ -10590,13 +10591,25 @@ function parseNumberInput(value) {
10590
10591
  }
10591
10592
  return parsed;
10592
10593
  }
10593
- function isDateWithinTask(task, date) {
10594
+ function getDateOnlyMs(date) {
10595
+ return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
10596
+ }
10597
+ function getPlannedIndexRange(task, rangeStartMs, rangeLength) {
10598
+ if (rangeLength <= 0) return null;
10594
10599
  const start = parseUTCDate(task.startDate);
10595
10600
  const end = parseUTCDate(task.endDate);
10596
- const dateMs = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
10597
- const startMs = Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate());
10598
- const endMs = Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate());
10599
- return startMs <= dateMs && dateMs <= endMs;
10601
+ const startIndex = Math.ceil((getDateOnlyMs(start) - rangeStartMs) / DAY_MS5);
10602
+ const endIndex = Math.floor((getDateOnlyMs(end) - rangeStartMs) / DAY_MS5);
10603
+ const clampedStartIndex = Math.max(0, startIndex);
10604
+ const clampedEndIndex = Math.min(rangeLength - 1, endIndex);
10605
+ if (clampedStartIndex > clampedEndIndex) return null;
10606
+ return {
10607
+ startIndex: clampedStartIndex,
10608
+ endIndex: clampedEndIndex
10609
+ };
10610
+ }
10611
+ function isDateIndexWithinPlannedRange(dateIndex, range) {
10612
+ return !!range && range.startIndex <= dateIndex && dateIndex <= range.endIndex;
10600
10613
  }
10601
10614
  function formatValue(value) {
10602
10615
  if (value === void 0) return "";
@@ -10695,6 +10708,261 @@ function PlanFactCellEditor({
10695
10708
  }
10696
10709
  );
10697
10710
  }
10711
+ function getCellSignatureForTask(cell, taskId) {
10712
+ return cell?.taskId === taskId ? `${cell.dateIndex}:${cell.kind}` : "";
10713
+ }
10714
+ function getEditingCellSignatureForTask(cell, taskId) {
10715
+ return cell?.taskId === taskId ? `${cell.dateIndex}:${cell.kind}:${cell.startValue ?? ""}` : "";
10716
+ }
10717
+ function getRangeAnchorSignatureForTask(range, taskId) {
10718
+ return range?.anchor.taskId === taskId ? `${range.anchor.dateIndex}:${range.anchor.kind}` : "";
10719
+ }
10720
+ function doesRangeTouchRow(bounds, rowIndex) {
10721
+ if (!bounds) return false;
10722
+ const firstSubrowIndex = rowIndex * 2;
10723
+ const lastSubrowIndex = firstSubrowIndex + 1;
10724
+ return bounds.fromSubrowIndex <= lastSubrowIndex && bounds.toSubrowIndex >= firstSubrowIndex;
10725
+ }
10726
+ function areRangeBoundsEqual(left, right) {
10727
+ if (left === right) return true;
10728
+ if (!left || !right) return false;
10729
+ return left.fromDateIndex === right.fromDateIndex && left.toDateIndex === right.toDateIndex && left.fromSubrowIndex === right.fromSubrowIndex && left.toSubrowIndex === right.toSubrowIndex;
10730
+ }
10731
+ function PlanFactRowInner({
10732
+ task,
10733
+ rowIndex,
10734
+ dateRange,
10735
+ dateKeys,
10736
+ renderedDateIndices,
10737
+ rowHeight,
10738
+ subrowHeight,
10739
+ dayWidth,
10740
+ plannedRange,
10741
+ todayDateIndex,
10742
+ isParent,
10743
+ isHighlighted,
10744
+ selectedTaskId,
10745
+ activeCell,
10746
+ editingCell,
10747
+ selectedRange,
10748
+ renderedRangeBounds,
10749
+ didDragSelectRef,
10750
+ isSelectingRef,
10751
+ isFillDraggingRef,
10752
+ onTaskSelect,
10753
+ selectSingleCell,
10754
+ queueHoverCellUpdate,
10755
+ setActiveCell,
10756
+ setEditingCell,
10757
+ setFillRange,
10758
+ clearSelectedCells,
10759
+ commitCell,
10760
+ commitSelectedCells,
10761
+ moveActiveCell,
10762
+ extendSelectedRange,
10763
+ focusCell,
10764
+ showOverflowTooltip,
10765
+ hideOverflowTooltip
10766
+ }) {
10767
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
10768
+ "div",
10769
+ {
10770
+ "data-gantt-task-row-id": task.id,
10771
+ className: joinClasses2(
10772
+ "gantt-pf-row",
10773
+ isParent && "gantt-pf-row-parent",
10774
+ selectedTaskId === task.id && "gantt-pf-row-selected",
10775
+ isHighlighted && "gantt-pf-row-highlighted"
10776
+ ),
10777
+ style: {
10778
+ top: `${rowIndex * rowHeight}px`,
10779
+ height: `${rowHeight}px`,
10780
+ gridTemplateColumns: `repeat(${dateRange.length}, ${dayWidth}px)`,
10781
+ ["--gantt-pf-today-left"]: todayDateIndex !== void 0 && todayDateIndex >= 0 ? `${todayDateIndex * dayWidth}px` : void 0
10782
+ },
10783
+ onClick: () => onTaskSelect?.(task.id),
10784
+ children: renderedDateIndices.map((dateIndex) => {
10785
+ const dateKey = dateKeys[dateIndex];
10786
+ if (dateKey === void 0) return null;
10787
+ const planned = isDateIndexWithinPlannedRange(dateIndex, plannedRange);
10788
+ return ["plan", "fact"].map((kind) => {
10789
+ const subrowIndex = getSubrowIndex(rowIndex, kind);
10790
+ const isInRenderedRange = !!renderedRangeBounds && dateIndex >= renderedRangeBounds.fromDateIndex && dateIndex <= renderedRangeBounds.toDateIndex && subrowIndex >= renderedRangeBounds.fromSubrowIndex && subrowIndex <= renderedRangeBounds.toSubrowIndex;
10791
+ const planValue = task.planByDate?.[dateKey];
10792
+ const factValue = task.factByDate?.[dateKey];
10793
+ const value = kind === "plan" ? planValue : factValue;
10794
+ const factStatus = factValue === void 0 || planValue === void 0 ? null : factValue >= planValue ? "success" : "warning";
10795
+ const isActive = activeCell?.taskId === task.id && activeCell.dateIndex === dateIndex && activeCell.kind === kind;
10796
+ const isEditing = editingCell?.taskId === task.id && editingCell.dateIndex === dateIndex && editingCell.kind === kind;
10797
+ const currentCell = { taskId: task.id, dateIndex, kind };
10798
+ const isSelected = !isParent && isInRenderedRange;
10799
+ const showFillHandle = !isParent && !isEditing && isInRenderedRange && renderedRangeBounds !== null && dateIndex === renderedRangeBounds.toDateIndex && subrowIndex === renderedRangeBounds.toSubrowIndex;
10800
+ const isRangeAnchor = !showFillHandle && !isParent && selectedRange?.anchor.taskId === task.id && selectedRange.anchor.dateIndex === dateIndex && selectedRange.anchor.kind === kind;
10801
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
10802
+ "div",
10803
+ {
10804
+ "data-plan-fact-task-id": task.id,
10805
+ "data-plan-fact-date-index": dateIndex,
10806
+ "data-plan-fact-kind": kind,
10807
+ className: joinClasses2(
10808
+ "gantt-pf-cell",
10809
+ `gantt-pf-cell-${kind}`,
10810
+ planned && kind === "plan" && "gantt-pf-cell-planned",
10811
+ value !== void 0 && "gantt-pf-cell-hasValue",
10812
+ kind === "fact" && factStatus === "success" && "gantt-pf-cell-factSuccess",
10813
+ kind === "fact" && factStatus === "warning" && "gantt-pf-cell-factWarning",
10814
+ isSelected && "gantt-pf-cell-selected",
10815
+ isInRenderedRange && renderedRangeBounds !== null && dateIndex === renderedRangeBounds.fromDateIndex && "gantt-pf-cell-rangeLeft",
10816
+ isInRenderedRange && renderedRangeBounds !== null && dateIndex === renderedRangeBounds.toDateIndex && "gantt-pf-cell-rangeRight",
10817
+ isInRenderedRange && renderedRangeBounds !== null && subrowIndex === renderedRangeBounds.fromSubrowIndex && "gantt-pf-cell-rangeTop",
10818
+ isInRenderedRange && renderedRangeBounds !== null && subrowIndex === renderedRangeBounds.toSubrowIndex && "gantt-pf-cell-rangeBottom",
10819
+ isRangeAnchor && "gantt-pf-cell-rangeAnchor",
10820
+ isActive && "gantt-pf-cell-active",
10821
+ isEditing && "gantt-pf-cell-editing",
10822
+ isParent && "gantt-pf-cell-readonly"
10823
+ ),
10824
+ style: {
10825
+ gridColumn: dateIndex + 1,
10826
+ gridRow: kind === "plan" ? 1 : 2,
10827
+ height: `${subrowHeight}px`
10828
+ },
10829
+ tabIndex: isParent ? -1 : 0,
10830
+ onMouseDown: (event) => {
10831
+ if (isParent) return;
10832
+ event.preventDefault();
10833
+ event.stopPropagation();
10834
+ didDragSelectRef.current = false;
10835
+ isSelectingRef.current = true;
10836
+ selectSingleCell(currentCell);
10837
+ onTaskSelect?.(task.id);
10838
+ event.currentTarget.focus();
10839
+ },
10840
+ onMouseEnter: () => {
10841
+ if (isParent) return;
10842
+ if (!isFillDraggingRef.current && !isSelectingRef.current) return;
10843
+ queueHoverCellUpdate(currentCell);
10844
+ },
10845
+ onFocus: () => {
10846
+ if (isParent) return;
10847
+ setActiveCell({ taskId: task.id, dateIndex, kind });
10848
+ },
10849
+ onClick: (event) => {
10850
+ event.stopPropagation();
10851
+ if (didDragSelectRef.current) {
10852
+ didDragSelectRef.current = false;
10853
+ return;
10854
+ }
10855
+ onTaskSelect?.(task.id);
10856
+ if (isParent) return;
10857
+ selectSingleCell(currentCell);
10858
+ event.currentTarget.focus();
10859
+ },
10860
+ onDoubleClick: (event) => {
10861
+ event.stopPropagation();
10862
+ if (isParent) return;
10863
+ setEditingCell({ taskId: task.id, dateIndex, kind });
10864
+ },
10865
+ onKeyDown: (event) => {
10866
+ if (isParent || isEditing) return;
10867
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
10868
+ event.preventDefault();
10869
+ event.stopPropagation();
10870
+ const direction = event.key.replace("Arrow", "").toLowerCase();
10871
+ if (event.shiftKey) {
10872
+ extendSelectedRange(selectedRange?.focus ?? currentCell, direction);
10873
+ } else {
10874
+ moveActiveCell(currentCell, direction);
10875
+ }
10876
+ return;
10877
+ }
10878
+ if (event.key === "Enter" || event.key === "F2") {
10879
+ event.preventDefault();
10880
+ event.stopPropagation();
10881
+ setEditingCell(selectedRange?.anchor ?? currentCell);
10882
+ return;
10883
+ }
10884
+ if (event.key === "Backspace" || event.key === "Delete") {
10885
+ event.preventDefault();
10886
+ event.stopPropagation();
10887
+ clearSelectedCells();
10888
+ return;
10889
+ }
10890
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
10891
+ event.preventDefault();
10892
+ event.stopPropagation();
10893
+ setEditingCell({ ...currentCell, startValue: event.key });
10894
+ }
10895
+ },
10896
+ children: [
10897
+ isEditing ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
10898
+ PlanFactCellEditor,
10899
+ {
10900
+ value,
10901
+ startValue: editingCell.startValue,
10902
+ onCommit: (nextValue) => {
10903
+ commitCell(task, dateIndex, kind, nextValue);
10904
+ setEditingCell(null);
10905
+ const nextActiveCell = { taskId: task.id, dateIndex, kind };
10906
+ selectSingleCell(nextActiveCell);
10907
+ focusCell(nextActiveCell);
10908
+ },
10909
+ onCommitRange: (nextValue) => {
10910
+ commitSelectedCells(nextValue);
10911
+ setEditingCell(null);
10912
+ const nextActiveCell = { taskId: task.id, dateIndex, kind };
10913
+ selectSingleCell(nextActiveCell);
10914
+ focusCell(nextActiveCell);
10915
+ },
10916
+ onCancel: () => {
10917
+ setEditingCell(null);
10918
+ const nextActiveCell = { taskId: task.id, dateIndex, kind };
10919
+ selectSingleCell(nextActiveCell);
10920
+ focusCell(nextActiveCell);
10921
+ }
10922
+ }
10923
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
10924
+ "span",
10925
+ {
10926
+ className: "gantt-pf-cellValue",
10927
+ onMouseEnter: (event) => {
10928
+ if (isParent || value === void 0) return;
10929
+ const compactValue = formatValue(value);
10930
+ const fullValue = formatTooltipValue(value);
10931
+ showOverflowTooltip(event.currentTarget, fullValue, compactValue !== fullValue);
10932
+ },
10933
+ onMouseLeave: hideOverflowTooltip,
10934
+ children: isParent ? "" : formatValue(value)
10935
+ }
10936
+ ),
10937
+ showFillHandle && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
10938
+ "span",
10939
+ {
10940
+ className: "gantt-pf-fillHandle",
10941
+ "aria-hidden": "true",
10942
+ onMouseDown: (event) => {
10943
+ event.preventDefault();
10944
+ event.stopPropagation();
10945
+ isSelectingRef.current = false;
10946
+ isFillDraggingRef.current = true;
10947
+ setFillRange(selectedRange);
10948
+ }
10949
+ }
10950
+ )
10951
+ ]
10952
+ },
10953
+ `${task.id}:${dateKey}:${kind}`
10954
+ );
10955
+ });
10956
+ })
10957
+ }
10958
+ );
10959
+ }
10960
+ function arePlanFactRowsEqual(previous, next) {
10961
+ const previousRangeTouchesRow = doesRangeTouchRow(previous.renderedRangeBounds, previous.rowIndex);
10962
+ const nextRangeTouchesRow = doesRangeTouchRow(next.renderedRangeBounds, next.rowIndex);
10963
+ return previous.task === next.task && previous.rowIndex === next.rowIndex && previous.dateRange === next.dateRange && previous.dateKeys === next.dateKeys && previous.renderedDateIndices === next.renderedDateIndices && previous.rowHeight === next.rowHeight && previous.subrowHeight === next.subrowHeight && previous.dayWidth === next.dayWidth && previous.plannedRange === next.plannedRange && previous.todayDateIndex === next.todayDateIndex && previous.isParent === next.isParent && previous.isHighlighted === next.isHighlighted && previous.selectedTaskId === previous.task.id === (next.selectedTaskId === next.task.id) && getCellSignatureForTask(previous.activeCell, previous.task.id) === getCellSignatureForTask(next.activeCell, next.task.id) && getEditingCellSignatureForTask(previous.editingCell, previous.task.id) === getEditingCellSignatureForTask(next.editingCell, next.task.id) && getRangeAnchorSignatureForTask(previous.selectedRange, previous.task.id) === getRangeAnchorSignatureForTask(next.selectedRange, next.task.id) && previousRangeTouchesRow === nextRangeTouchesRow && (!nextRangeTouchesRow || areRangeBoundsEqual(previous.renderedRangeBounds, next.renderedRangeBounds));
10964
+ }
10965
+ var PlanFactRow = import_react17.default.memo(PlanFactRowInner, arePlanFactRowsEqual);
10698
10966
  function PlanFactMatrix({
10699
10967
  tasks,
10700
10968
  allTasks = tasks,
@@ -10708,7 +10976,10 @@ function PlanFactMatrix({
10708
10976
  onTasksChange,
10709
10977
  onCellCommit,
10710
10978
  highlightedTaskIds,
10711
- filterMode = "highlight"
10979
+ filterMode = "highlight",
10980
+ visibleRowIndices,
10981
+ visibleDateIndices,
10982
+ todayDateIndex
10712
10983
  }) {
10713
10984
  const [activeCell, setActiveCell] = (0, import_react17.useState)(null);
10714
10985
  const [editingCell, setEditingCell] = (0, import_react17.useState)(null);
@@ -10718,10 +10989,20 @@ function PlanFactMatrix({
10718
10989
  const isSelectingRef = (0, import_react17.useRef)(false);
10719
10990
  const isFillDraggingRef = (0, import_react17.useRef)(false);
10720
10991
  const didDragSelectRef = (0, import_react17.useRef)(false);
10992
+ const pendingHoverCellRef = (0, import_react17.useRef)(null);
10993
+ const hoverFrameRef = (0, import_react17.useRef)(null);
10721
10994
  const rootRef = (0, import_react17.useRef)(null);
10722
10995
  const bodyRef = (0, import_react17.useRef)(null);
10723
10996
  const totalWidth = dateRange.length * dayWidth;
10724
10997
  const subrowHeight = rowHeight / 2;
10998
+ const renderedRowIndices = (0, import_react17.useMemo)(
10999
+ () => visibleRowIndices ?? tasks.map((_, index) => index),
11000
+ [tasks, visibleRowIndices]
11001
+ );
11002
+ const renderedDateIndices = (0, import_react17.useMemo)(
11003
+ () => visibleDateIndices ?? dateRange.map((_, index) => index),
11004
+ [dateRange, visibleDateIndices]
11005
+ );
10725
11006
  const parentTaskIds = (0, import_react17.useMemo)(() => {
10726
11007
  const ids = /* @__PURE__ */ new Set();
10727
11008
  for (const task of allTasks) {
@@ -10730,11 +11011,32 @@ function PlanFactMatrix({
10730
11011
  return ids;
10731
11012
  }, [allTasks]);
10732
11013
  const dateKeys = (0, import_react17.useMemo)(() => dateRange.map(formatDateKey), [dateRange]);
11014
+ const dateRangeStartMs = dateRange[0] ? getDateOnlyMs(dateRange[0]) : 0;
10733
11015
  const taskIndexById = (0, import_react17.useMemo)(() => {
10734
11016
  const indexById = /* @__PURE__ */ new Map();
10735
11017
  tasks.forEach((task, index) => indexById.set(task.id, index));
10736
11018
  return indexById;
10737
11019
  }, [tasks]);
11020
+ const taskById = (0, import_react17.useMemo)(
11021
+ () => new Map(tasks.map((task) => [task.id, task])),
11022
+ [tasks]
11023
+ );
11024
+ const monthSeparatorIndices = (0, import_react17.useMemo)(() => {
11025
+ const indices = [];
11026
+ for (let index = 1; index < dateRange.length; index += 1) {
11027
+ if (dateRange[index].getUTCDate() === 1) {
11028
+ indices.push(index);
11029
+ }
11030
+ }
11031
+ return indices;
11032
+ }, [dateRange]);
11033
+ const plannedRangeByTaskId = (0, import_react17.useMemo)(() => {
11034
+ const rangeByTaskId = /* @__PURE__ */ new Map();
11035
+ for (const task of tasks) {
11036
+ rangeByTaskId.set(task.id, getPlannedIndexRange(task, dateRangeStartMs, dateRange.length));
11037
+ }
11038
+ return rangeByTaskId;
11039
+ }, [dateRange.length, dateRangeStartMs, tasks]);
10738
11040
  const focusCell = (0, import_react17.useCallback)((cell) => {
10739
11041
  window.requestAnimationFrame(() => {
10740
11042
  const selector = [
@@ -10746,6 +11048,11 @@ function PlanFactMatrix({
10746
11048
  });
10747
11049
  }, []);
10748
11050
  const clearSelection = (0, import_react17.useCallback)(() => {
11051
+ if (hoverFrameRef.current !== null) {
11052
+ window.cancelAnimationFrame(hoverFrameRef.current);
11053
+ hoverFrameRef.current = null;
11054
+ }
11055
+ pendingHoverCellRef.current = null;
10749
11056
  isSelectingRef.current = false;
10750
11057
  isFillDraggingRef.current = false;
10751
11058
  didDragSelectRef.current = false;
@@ -10758,6 +11065,29 @@ function PlanFactMatrix({
10758
11065
  const hideOverflowTooltip = (0, import_react17.useCallback)(() => {
10759
11066
  setOverflowTooltip(null);
10760
11067
  }, []);
11068
+ const flushPendingHoverCell = (0, import_react17.useCallback)(() => {
11069
+ hoverFrameRef.current = null;
11070
+ const currentCell = pendingHoverCellRef.current;
11071
+ if (!currentCell) return;
11072
+ if (isFillDraggingRef.current && selectedRange) {
11073
+ setFillRange({ anchor: selectedRange.anchor, focus: currentCell });
11074
+ setActiveCell(currentCell);
11075
+ onTaskSelect?.(currentCell.taskId);
11076
+ return;
11077
+ }
11078
+ if (!isSelectingRef.current) return;
11079
+ didDragSelectRef.current = true;
11080
+ setSelectedRange((currentRange) => ({
11081
+ anchor: currentRange?.anchor ?? currentCell,
11082
+ focus: currentCell
11083
+ }));
11084
+ onTaskSelect?.(currentCell.taskId);
11085
+ }, [onTaskSelect, selectedRange]);
11086
+ const queueHoverCellUpdate = (0, import_react17.useCallback)((cell) => {
11087
+ pendingHoverCellRef.current = cell;
11088
+ if (hoverFrameRef.current !== null) return;
11089
+ hoverFrameRef.current = window.requestAnimationFrame(flushPendingHoverCell);
11090
+ }, [flushPendingHoverCell]);
10761
11091
  const showOverflowTooltip = (0, import_react17.useCallback)((target, label, force = false) => {
10762
11092
  if (!rootRef.current || !label) return;
10763
11093
  if (!force && target.scrollWidth <= target.clientWidth) {
@@ -10796,6 +11126,11 @@ function PlanFactMatrix({
10796
11126
  )
10797
11127
  };
10798
11128
  }, [taskIndexById]);
11129
+ const renderedRange = fillRange ?? selectedRange;
11130
+ const renderedRangeBounds = (0, import_react17.useMemo)(
11131
+ () => renderedRange ? getRangeBounds(renderedRange) : null,
11132
+ [getRangeBounds, renderedRange]
11133
+ );
10799
11134
  const getCellFromPosition = (0, import_react17.useCallback)((subrowIndex, dateIndex) => {
10800
11135
  const task = tasks[Math.floor(subrowIndex / 2)];
10801
11136
  if (!task) return null;
@@ -10815,33 +11150,6 @@ function PlanFactMatrix({
10815
11150
  const cellSubrowIndex = getSubrowIndex(cellTaskIndex, cell.kind);
10816
11151
  return cell.dateIndex >= bounds.fromDateIndex && cell.dateIndex <= bounds.toDateIndex && cellSubrowIndex >= bounds.fromSubrowIndex && cellSubrowIndex <= bounds.toSubrowIndex;
10817
11152
  }, [getRangeBounds, taskIndexById]);
10818
- const isCellInSelectedRange = (0, import_react17.useCallback)((cell) => {
10819
- if (!selectedRange) return false;
10820
- return isCellInRange(cell, fillRange ?? selectedRange);
10821
- }, [fillRange, isCellInRange, selectedRange]);
10822
- const getSelectedRangeEdgeClasses = (0, import_react17.useCallback)((cell) => {
10823
- if (!selectedRange) return [];
10824
- const range = fillRange ?? selectedRange;
10825
- const bounds = getRangeBounds(range);
10826
- const taskIndex = taskIndexById.get(cell.taskId);
10827
- if (!bounds || taskIndex === void 0 || !isCellInRange(cell, range)) return [];
10828
- const subrowIndex = getSubrowIndex(taskIndex, cell.kind);
10829
- return [
10830
- cell.dateIndex === bounds.fromDateIndex && "gantt-pf-cell-rangeLeft",
10831
- cell.dateIndex === bounds.toDateIndex && "gantt-pf-cell-rangeRight",
10832
- subrowIndex === bounds.fromSubrowIndex && "gantt-pf-cell-rangeTop",
10833
- subrowIndex === bounds.toSubrowIndex && "gantt-pf-cell-rangeBottom"
10834
- ];
10835
- }, [fillRange, getRangeBounds, isCellInRange, selectedRange, taskIndexById]);
10836
- const isFillHandleCell = (0, import_react17.useCallback)((cell) => {
10837
- if (!selectedRange) return false;
10838
- const range = fillRange ?? selectedRange;
10839
- const bounds = getRangeBounds(range);
10840
- const taskIndex = taskIndexById.get(cell.taskId);
10841
- if (!bounds || taskIndex === void 0 || !isCellInRange(cell, range)) return false;
10842
- const subrowIndex = getSubrowIndex(taskIndex, cell.kind);
10843
- return cell.dateIndex === bounds.toDateIndex && subrowIndex === bounds.toSubrowIndex;
10844
- }, [fillRange, getRangeBounds, isCellInRange, selectedRange, taskIndexById]);
10845
11153
  const commitCell = (0, import_react17.useCallback)((task, dateIndex, kind, value) => {
10846
11154
  const dateKey = dateKeys[dateIndex];
10847
11155
  const source = kind === "plan" ? task.planByDate : task.factByDate;
@@ -10864,40 +11172,45 @@ function PlanFactMatrix({
10864
11172
  value
10865
11173
  });
10866
11174
  }, [dateKeys, dateRange, onCellCommit, onTasksChange]);
10867
- const clearSelectedCells = (0, import_react17.useCallback)(() => {
10868
- if (!selectedRange) {
10869
- if (!activeCell) return;
10870
- const task = tasks.find((candidate) => candidate.id === activeCell.taskId);
10871
- if (!task || parentTaskIds.has(task.id)) return;
10872
- commitCell(task, activeCell.dateIndex, activeCell.kind, void 0);
10873
- return;
10874
- }
11175
+ const commitRangeCells = (0, import_react17.useCallback)((bounds, value, mode) => {
10875
11176
  const changedTasksById = /* @__PURE__ */ new Map();
10876
- for (const task of tasks) {
10877
- if (parentTaskIds.has(task.id)) continue;
10878
- let nextPlanByDate = task.planByDate;
10879
- let nextFactByDate = task.factByDate;
11177
+ for (let subrowIndex = bounds.fromSubrowIndex; subrowIndex <= bounds.toSubrowIndex; subrowIndex += 1) {
11178
+ const task = tasks[Math.floor(subrowIndex / 2)];
11179
+ if (!task || parentTaskIds.has(task.id)) continue;
11180
+ const kind = subrowIndex % 2 === 0 ? "plan" : "fact";
11181
+ const currentChangedTask = changedTasksById.get(task.id) ?? task;
11182
+ let nextPlanByDate = currentChangedTask.planByDate;
11183
+ let nextFactByDate = currentChangedTask.factByDate;
10880
11184
  let didChange = false;
10881
- for (let dateIndex = 0; dateIndex < dateKeys.length; dateIndex += 1) {
11185
+ for (let dateIndex = bounds.fromDateIndex; dateIndex <= bounds.toDateIndex; dateIndex += 1) {
10882
11186
  const dateKey = dateKeys[dateIndex];
10883
- const planCell = { taskId: task.id, dateIndex, kind: "plan" };
10884
- if (isCellInSelectedRange(planCell) && nextPlanByDate?.[dateKey] !== void 0) {
11187
+ if (dateKey === void 0) continue;
11188
+ const currentValues = kind === "plan" ? nextPlanByDate : nextFactByDate;
11189
+ const currentValue = currentValues?.[dateKey];
11190
+ const nextValue = mode === "clear" ? void 0 : value;
11191
+ if (currentValue === nextValue) continue;
11192
+ if (kind === "plan") {
10885
11193
  nextPlanByDate = { ...nextPlanByDate ?? {} };
10886
- delete nextPlanByDate[dateKey];
10887
- didChange = true;
10888
- }
10889
- const factCell = { taskId: task.id, dateIndex, kind: "fact" };
10890
- if (isCellInSelectedRange(factCell) && nextFactByDate?.[dateKey] !== void 0) {
11194
+ if (nextValue === void 0) {
11195
+ delete nextPlanByDate[dateKey];
11196
+ } else {
11197
+ nextPlanByDate[dateKey] = nextValue;
11198
+ }
11199
+ } else {
10891
11200
  nextFactByDate = { ...nextFactByDate ?? {} };
10892
- delete nextFactByDate[dateKey];
10893
- didChange = true;
11201
+ if (nextValue === void 0) {
11202
+ delete nextFactByDate[dateKey];
11203
+ } else {
11204
+ nextFactByDate[dateKey] = nextValue;
11205
+ }
10894
11206
  }
11207
+ didChange = true;
10895
11208
  }
10896
11209
  if (didChange) {
10897
11210
  changedTasksById.set(task.id, {
10898
- ...task,
10899
- ...nextPlanByDate !== task.planByDate ? { planByDate: nextPlanByDate ?? {} } : {},
10900
- ...nextFactByDate !== task.factByDate ? { factByDate: nextFactByDate ?? {} } : {}
11211
+ ...currentChangedTask,
11212
+ ...nextPlanByDate !== currentChangedTask.planByDate ? { planByDate: nextPlanByDate ?? {} } : {},
11213
+ ...nextFactByDate !== currentChangedTask.factByDate ? { factByDate: nextFactByDate ?? {} } : {}
10901
11214
  });
10902
11215
  }
10903
11216
  }
@@ -10905,67 +11218,46 @@ function PlanFactMatrix({
10905
11218
  if (changedTasks.length > 0) {
10906
11219
  onTasksChange?.(changedTasks);
10907
11220
  }
10908
- }, [activeCell, commitCell, dateKeys, isCellInSelectedRange, onTasksChange, parentTaskIds, selectedRange, tasks]);
11221
+ }, [dateKeys, onTasksChange, parentTaskIds, tasks]);
11222
+ const clearSelectedCells = (0, import_react17.useCallback)(() => {
11223
+ const activeRange = fillRange ?? selectedRange;
11224
+ if (!activeRange) {
11225
+ if (!activeCell) return;
11226
+ const task = taskById.get(activeCell.taskId);
11227
+ if (!task || parentTaskIds.has(task.id)) return;
11228
+ commitCell(task, activeCell.dateIndex, activeCell.kind, void 0);
11229
+ return;
11230
+ }
11231
+ const bounds = getRangeBounds(activeRange);
11232
+ if (bounds) {
11233
+ commitRangeCells(bounds, void 0, "clear");
11234
+ }
11235
+ }, [activeCell, commitCell, commitRangeCells, fillRange, getRangeBounds, parentTaskIds, selectedRange, taskById]);
10909
11236
  const commitSelectedCells = (0, import_react17.useCallback)((value) => {
10910
- if (!selectedRange) {
11237
+ const activeRange = fillRange ?? selectedRange;
11238
+ if (!activeRange) {
10911
11239
  if (!activeCell) return;
10912
- const task = tasks.find((candidate) => candidate.id === activeCell.taskId);
11240
+ const task = taskById.get(activeCell.taskId);
10913
11241
  if (!task || parentTaskIds.has(task.id)) return;
10914
11242
  commitCell(task, activeCell.dateIndex, activeCell.kind, value);
10915
11243
  return;
10916
11244
  }
10917
- const changedTasksById = /* @__PURE__ */ new Map();
10918
- for (const task of tasks) {
10919
- if (parentTaskIds.has(task.id)) continue;
10920
- let nextPlanByDate = task.planByDate;
10921
- let nextFactByDate = task.factByDate;
10922
- let didChange = false;
10923
- for (let dateIndex = 0; dateIndex < dateKeys.length; dateIndex += 1) {
10924
- const dateKey = dateKeys[dateIndex];
10925
- const planCell = { taskId: task.id, dateIndex, kind: "plan" };
10926
- if (isCellInSelectedRange(planCell) && nextPlanByDate?.[dateKey] !== value) {
10927
- nextPlanByDate = { ...nextPlanByDate ?? {} };
10928
- if (value === void 0) {
10929
- delete nextPlanByDate[dateKey];
10930
- } else {
10931
- nextPlanByDate[dateKey] = value;
10932
- }
10933
- didChange = true;
10934
- }
10935
- const factCell = { taskId: task.id, dateIndex, kind: "fact" };
10936
- if (isCellInSelectedRange(factCell) && nextFactByDate?.[dateKey] !== value) {
10937
- nextFactByDate = { ...nextFactByDate ?? {} };
10938
- if (value === void 0) {
10939
- delete nextFactByDate[dateKey];
10940
- } else {
10941
- nextFactByDate[dateKey] = value;
10942
- }
10943
- didChange = true;
10944
- }
10945
- }
10946
- if (didChange) {
10947
- changedTasksById.set(task.id, {
10948
- ...task,
10949
- ...nextPlanByDate !== task.planByDate ? { planByDate: nextPlanByDate ?? {} } : {},
10950
- ...nextFactByDate !== task.factByDate ? { factByDate: nextFactByDate ?? {} } : {}
10951
- });
10952
- }
10953
- }
10954
- const changedTasks = Array.from(changedTasksById.values());
10955
- if (changedTasks.length > 0) {
10956
- onTasksChange?.(changedTasks);
11245
+ const bounds = getRangeBounds(activeRange);
11246
+ if (bounds) {
11247
+ commitRangeCells(bounds, value, "set");
10957
11248
  }
10958
- }, [activeCell, commitCell, dateKeys, isCellInSelectedRange, onTasksChange, parentTaskIds, selectedRange, tasks]);
11249
+ }, [activeCell, commitCell, commitRangeCells, fillRange, getRangeBounds, parentTaskIds, selectedRange, taskById]);
10959
11250
  const getCellValue = (0, import_react17.useCallback)((cell) => {
10960
- const task = tasks.find((candidate) => candidate.id === cell.taskId);
11251
+ const task = taskById.get(cell.taskId);
10961
11252
  if (!task) return void 0;
10962
11253
  const dateKey = dateKeys[cell.dateIndex];
10963
11254
  return cell.kind === "plan" ? task.planByDate?.[dateKey] : task.factByDate?.[dateKey];
10964
- }, [dateKeys, tasks]);
10965
- const applyFillRange = (0, import_react17.useCallback)(() => {
10966
- if (!selectedRange || !fillRange) return;
11255
+ }, [dateKeys, taskById]);
11256
+ const applyFillRange = (0, import_react17.useCallback)((nextFillRange) => {
11257
+ const targetRange = nextFillRange ?? fillRange;
11258
+ if (!selectedRange || !targetRange) return;
10967
11259
  const sourceBounds = getRangeBounds(selectedRange);
10968
- const targetBounds = getRangeBounds(fillRange);
11260
+ const targetBounds = getRangeBounds(targetRange);
10969
11261
  if (!sourceBounds || !targetBounds) return;
10970
11262
  const sourceDateSpan = sourceBounds.toDateIndex - sourceBounds.fromDateIndex + 1;
10971
11263
  const sourceSubrowSpan = sourceBounds.toSubrowIndex - sourceBounds.fromSubrowIndex + 1;
@@ -10973,7 +11265,7 @@ function PlanFactMatrix({
10973
11265
  for (let subrowIndex = targetBounds.fromSubrowIndex; subrowIndex <= targetBounds.toSubrowIndex; subrowIndex += 1) {
10974
11266
  const targetCellForRow = getCellFromPosition(subrowIndex, targetBounds.fromDateIndex);
10975
11267
  if (!targetCellForRow || parentTaskIds.has(targetCellForRow.taskId)) continue;
10976
- const originalTask = tasks.find((task) => task.id === targetCellForRow.taskId);
11268
+ const originalTask = taskById.get(targetCellForRow.taskId);
10977
11269
  if (!originalTask) continue;
10978
11270
  let changedTask = changedTasksById.get(originalTask.id) ?? originalTask;
10979
11271
  let nextPlanByDate = changedTask.planByDate;
@@ -11019,8 +11311,8 @@ function PlanFactMatrix({
11019
11311
  if (changedTasks.length > 0) {
11020
11312
  onTasksChange?.(changedTasks);
11021
11313
  }
11022
- setSelectedRange(fillRange);
11023
- setActiveCell(fillRange.anchor);
11314
+ setSelectedRange(targetRange);
11315
+ setActiveCell(targetRange.anchor);
11024
11316
  setFillRange(null);
11025
11317
  }, [
11026
11318
  dateKeys,
@@ -11032,7 +11324,7 @@ function PlanFactMatrix({
11032
11324
  onTasksChange,
11033
11325
  parentTaskIds,
11034
11326
  selectedRange,
11035
- tasks
11327
+ taskById
11036
11328
  ]);
11037
11329
  const moveActiveCell = (0, import_react17.useCallback)((cell, direction) => {
11038
11330
  const taskIndex = tasks.findIndex((task) => task.id === cell.taskId);
@@ -11104,9 +11396,19 @@ function PlanFactMatrix({
11104
11396
  }, [dateRange.length, focusCell, onTaskSelect, parentTaskIds, selectedRange, tasks]);
11105
11397
  (0, import_react17.useEffect)(() => {
11106
11398
  const endSelection = () => {
11399
+ let pendingFillRange = null;
11400
+ if (hoverFrameRef.current !== null) {
11401
+ window.cancelAnimationFrame(hoverFrameRef.current);
11402
+ hoverFrameRef.current = null;
11403
+ const pendingCell = pendingHoverCellRef.current;
11404
+ if (pendingCell && isFillDraggingRef.current && selectedRange) {
11405
+ pendingFillRange = { anchor: selectedRange.anchor, focus: pendingCell };
11406
+ }
11407
+ flushPendingHoverCell();
11408
+ }
11107
11409
  if (isFillDraggingRef.current) {
11108
11410
  isFillDraggingRef.current = false;
11109
- applyFillRange();
11411
+ applyFillRange(pendingFillRange);
11110
11412
  }
11111
11413
  isSelectingRef.current = false;
11112
11414
  };
@@ -11114,7 +11416,12 @@ function PlanFactMatrix({
11114
11416
  return () => {
11115
11417
  window.removeEventListener("mouseup", endSelection);
11116
11418
  };
11117
- }, [applyFillRange]);
11419
+ }, [applyFillRange, flushPendingHoverCell]);
11420
+ (0, import_react17.useEffect)(() => () => {
11421
+ if (hoverFrameRef.current !== null) {
11422
+ window.cancelAnimationFrame(hoverFrameRef.current);
11423
+ }
11424
+ }, []);
11118
11425
  (0, import_react17.useEffect)(() => {
11119
11426
  const handleKeyDown = (event) => {
11120
11427
  if (event.key !== "Escape") return;
@@ -11144,15 +11451,39 @@ function PlanFactMatrix({
11144
11451
  ["--gantt-pf-day-width"]: `${dayWidth}px`
11145
11452
  },
11146
11453
  children: [
11147
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "gantt-pf-header", style: { width: `${totalWidth}px`, height: `${headerHeight}px` }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11148
- TimeScaleHeader_default,
11454
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "gantt-pf-header", style: { width: `${totalWidth}px`, height: `${headerHeight}px` }, children: [
11455
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11456
+ TimeScaleHeader_default,
11457
+ {
11458
+ days: dateRange,
11459
+ dayWidth,
11460
+ headerHeight: headerHeight - 1,
11461
+ viewMode: "day"
11462
+ }
11463
+ ),
11464
+ todayDateIndex !== void 0 && todayDateIndex >= 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11465
+ "span",
11466
+ {
11467
+ className: "gantt-pf-headerTodayLine",
11468
+ "aria-hidden": "true",
11469
+ style: {
11470
+ left: `${todayDateIndex * dayWidth}px`,
11471
+ top: `${Math.max(0, headerHeight / 2)}px`
11472
+ }
11473
+ }
11474
+ )
11475
+ ] }),
11476
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "gantt-pf-monthSeparatorLayer", "aria-hidden": "true", children: monthSeparatorIndices.map((dateIndex) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11477
+ "span",
11149
11478
  {
11150
- days: dateRange,
11151
- dayWidth,
11152
- headerHeight: headerHeight - 1,
11153
- viewMode: "day"
11154
- }
11155
- ) }),
11479
+ className: "gantt-pf-monthSeparator",
11480
+ style: {
11481
+ left: `${Math.round(dateIndex * dayWidth)}px`,
11482
+ top: `${Math.max(0, headerHeight / 2)}px`
11483
+ }
11484
+ },
11485
+ `month-separator-${dateIndex}`
11486
+ )) }),
11156
11487
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11157
11488
  "div",
11158
11489
  {
@@ -11163,202 +11494,48 @@ function PlanFactMatrix({
11163
11494
  minHeight: bodyMinHeight,
11164
11495
  width: `${totalWidth}px`
11165
11496
  },
11166
- children: tasks.map((task, rowIndex) => {
11497
+ children: renderedRowIndices.map((rowIndex) => {
11498
+ const task = tasks[rowIndex];
11499
+ if (!task) return null;
11167
11500
  const isParent = parentTaskIds.has(task.id);
11168
11501
  const isHighlighted = filterMode === "highlight" && !!highlightedTaskIds?.has(task.id);
11169
11502
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11170
- "div",
11503
+ PlanFactRow,
11171
11504
  {
11172
- "data-gantt-task-row-id": task.id,
11173
- className: joinClasses2(
11174
- "gantt-pf-row",
11175
- isParent && "gantt-pf-row-parent",
11176
- selectedTaskId === task.id && "gantt-pf-row-selected",
11177
- isHighlighted && "gantt-pf-row-highlighted"
11178
- ),
11179
- style: {
11180
- top: `${rowIndex * rowHeight}px`,
11181
- height: `${rowHeight}px`,
11182
- gridTemplateColumns: `repeat(${dateRange.length}, ${dayWidth}px)`
11183
- },
11184
- onClick: () => onTaskSelect?.(task.id),
11185
- children: dateRange.map((date, dateIndex) => {
11186
- const dateKey = dateKeys[dateIndex];
11187
- const planned = isDateWithinTask(task, date);
11188
- return ["plan", "fact"].map((kind) => {
11189
- const planValue = task.planByDate?.[dateKey];
11190
- const factValue = task.factByDate?.[dateKey];
11191
- const value = kind === "plan" ? planValue : factValue;
11192
- const factStatus = factValue === void 0 || planValue === void 0 ? null : factValue >= planValue ? "success" : "warning";
11193
- const isActive = activeCell?.taskId === task.id && activeCell.dateIndex === dateIndex && activeCell.kind === kind;
11194
- const isEditing = editingCell?.taskId === task.id && editingCell.dateIndex === dateIndex && editingCell.kind === kind;
11195
- const currentCell = { taskId: task.id, dateIndex, kind };
11196
- const isSelected = !isParent && isCellInSelectedRange(currentCell);
11197
- const showFillHandle = !isParent && !isEditing && isFillHandleCell(currentCell);
11198
- const isRangeAnchor = !showFillHandle && !isParent && selectedRange?.anchor.taskId === task.id && selectedRange.anchor.dateIndex === dateIndex && selectedRange.anchor.kind === kind;
11199
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
11200
- "div",
11201
- {
11202
- "data-plan-fact-task-id": task.id,
11203
- "data-plan-fact-date-index": dateIndex,
11204
- "data-plan-fact-kind": kind,
11205
- className: joinClasses2(
11206
- "gantt-pf-cell",
11207
- `gantt-pf-cell-${kind}`,
11208
- planned && kind === "plan" && !isParent && "gantt-pf-cell-planned",
11209
- value !== void 0 && "gantt-pf-cell-hasValue",
11210
- kind === "fact" && factStatus === "success" && "gantt-pf-cell-factSuccess",
11211
- kind === "fact" && factStatus === "warning" && "gantt-pf-cell-factWarning",
11212
- isSelected && "gantt-pf-cell-selected",
11213
- ...getSelectedRangeEdgeClasses(currentCell),
11214
- isRangeAnchor && "gantt-pf-cell-rangeAnchor",
11215
- isActive && "gantt-pf-cell-active",
11216
- isEditing && "gantt-pf-cell-editing",
11217
- isParent && "gantt-pf-cell-readonly"
11218
- ),
11219
- style: {
11220
- gridColumn: dateIndex + 1,
11221
- gridRow: kind === "plan" ? 1 : 2,
11222
- height: `${subrowHeight}px`
11223
- },
11224
- tabIndex: isParent ? -1 : 0,
11225
- onMouseDown: (event) => {
11226
- if (isParent) return;
11227
- event.preventDefault();
11228
- event.stopPropagation();
11229
- didDragSelectRef.current = false;
11230
- isSelectingRef.current = true;
11231
- selectSingleCell(currentCell);
11232
- onTaskSelect?.(task.id);
11233
- event.currentTarget.focus();
11234
- },
11235
- onMouseEnter: () => {
11236
- if (!isParent && isFillDraggingRef.current && selectedRange) {
11237
- setFillRange({ anchor: selectedRange.anchor, focus: currentCell });
11238
- setActiveCell(currentCell);
11239
- onTaskSelect?.(task.id);
11240
- return;
11241
- }
11242
- if (isParent || !isSelectingRef.current) return;
11243
- didDragSelectRef.current = true;
11244
- setSelectedRange((currentRange) => ({
11245
- anchor: currentRange?.anchor ?? currentCell,
11246
- focus: currentCell
11247
- }));
11248
- onTaskSelect?.(task.id);
11249
- },
11250
- onFocus: () => {
11251
- if (isParent) return;
11252
- setActiveCell({ taskId: task.id, dateIndex, kind });
11253
- },
11254
- onClick: (event) => {
11255
- event.stopPropagation();
11256
- if (didDragSelectRef.current) {
11257
- didDragSelectRef.current = false;
11258
- return;
11259
- }
11260
- onTaskSelect?.(task.id);
11261
- if (isParent) return;
11262
- selectSingleCell(currentCell);
11263
- event.currentTarget.focus();
11264
- },
11265
- onDoubleClick: (event) => {
11266
- event.stopPropagation();
11267
- if (isParent) return;
11268
- setEditingCell({ taskId: task.id, dateIndex, kind });
11269
- },
11270
- onKeyDown: (event) => {
11271
- if (isParent || isEditing) return;
11272
- if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
11273
- event.preventDefault();
11274
- event.stopPropagation();
11275
- const direction = event.key.replace("Arrow", "").toLowerCase();
11276
- if (event.shiftKey) {
11277
- extendSelectedRange(selectedRange?.focus ?? currentCell, direction);
11278
- } else {
11279
- moveActiveCell(currentCell, direction);
11280
- }
11281
- return;
11282
- }
11283
- if (event.key === "Enter" || event.key === "F2") {
11284
- event.preventDefault();
11285
- event.stopPropagation();
11286
- setEditingCell(selectedRange?.anchor ?? currentCell);
11287
- return;
11288
- }
11289
- if (event.key === "Backspace" || event.key === "Delete") {
11290
- event.preventDefault();
11291
- event.stopPropagation();
11292
- clearSelectedCells();
11293
- return;
11294
- }
11295
- if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
11296
- event.preventDefault();
11297
- event.stopPropagation();
11298
- setEditingCell({ ...currentCell, startValue: event.key });
11299
- }
11300
- },
11301
- children: [
11302
- isEditing ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11303
- PlanFactCellEditor,
11304
- {
11305
- value,
11306
- startValue: editingCell.startValue,
11307
- onCommit: (nextValue) => {
11308
- commitCell(task, dateIndex, kind, nextValue);
11309
- setEditingCell(null);
11310
- const nextActiveCell = { taskId: task.id, dateIndex, kind };
11311
- selectSingleCell(nextActiveCell);
11312
- focusCell(nextActiveCell);
11313
- },
11314
- onCommitRange: (nextValue) => {
11315
- commitSelectedCells(nextValue);
11316
- setEditingCell(null);
11317
- const nextActiveCell = { taskId: task.id, dateIndex, kind };
11318
- setActiveCell(nextActiveCell);
11319
- focusCell(nextActiveCell);
11320
- },
11321
- onCancel: () => {
11322
- setEditingCell(null);
11323
- const nextActiveCell = { taskId: task.id, dateIndex, kind };
11324
- setActiveCell(nextActiveCell);
11325
- focusCell(nextActiveCell);
11326
- }
11327
- }
11328
- ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11329
- "span",
11330
- {
11331
- className: "gantt-pf-cellValue",
11332
- onMouseEnter: (event) => {
11333
- if (isParent || value === void 0) return;
11334
- const compactValue = formatValue(value);
11335
- const fullValue = formatTooltipValue(value);
11336
- showOverflowTooltip(event.currentTarget, fullValue, compactValue !== fullValue);
11337
- },
11338
- onMouseLeave: hideOverflowTooltip,
11339
- children: isParent ? "" : formatValue(value)
11340
- }
11341
- ),
11342
- showFillHandle && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
11343
- "span",
11344
- {
11345
- className: "gantt-pf-fillHandle",
11346
- "aria-hidden": "true",
11347
- onMouseDown: (event) => {
11348
- event.preventDefault();
11349
- event.stopPropagation();
11350
- isSelectingRef.current = false;
11351
- isFillDraggingRef.current = true;
11352
- setFillRange(selectedRange);
11353
- }
11354
- }
11355
- )
11356
- ]
11357
- },
11358
- `${task.id}:${dateKey}:${kind}`
11359
- );
11360
- });
11361
- })
11505
+ task,
11506
+ rowIndex,
11507
+ dateRange,
11508
+ dateKeys,
11509
+ renderedDateIndices,
11510
+ rowHeight,
11511
+ subrowHeight,
11512
+ dayWidth,
11513
+ plannedRange: plannedRangeByTaskId.get(task.id) ?? null,
11514
+ todayDateIndex,
11515
+ isParent,
11516
+ isHighlighted,
11517
+ selectedTaskId,
11518
+ activeCell,
11519
+ editingCell,
11520
+ selectedRange,
11521
+ renderedRangeBounds,
11522
+ didDragSelectRef,
11523
+ isSelectingRef,
11524
+ isFillDraggingRef,
11525
+ onTaskSelect,
11526
+ selectSingleCell,
11527
+ queueHoverCellUpdate,
11528
+ setActiveCell,
11529
+ setEditingCell,
11530
+ setFillRange,
11531
+ clearSelectedCells,
11532
+ commitCell,
11533
+ commitSelectedCells,
11534
+ moveActiveCell,
11535
+ extendSelectedRange,
11536
+ focusCell,
11537
+ showOverflowTooltip,
11538
+ hideOverflowTooltip
11362
11539
  },
11363
11540
  task.id
11364
11541
  );
@@ -11794,6 +11971,57 @@ function createTaskPreviewPositionStore() {
11794
11971
  var import_jsx_runtime19 = require("react/jsx-runtime");
11795
11972
  var SCROLL_TO_ROW_CONTEXT_ROWS = 2;
11796
11973
  var TASK_ROW_OVERSCAN = 8;
11974
+ var PLAN_FACT_COLUMN_OVERSCAN = 24;
11975
+ var PLAN_FACT_COLUMN_WINDOW_STEP = 14;
11976
+ function getFullMonthDays(tasks) {
11977
+ if (!tasks || tasks.length === 0) {
11978
+ return getMultiMonthDays(tasks);
11979
+ }
11980
+ let minDate = null;
11981
+ let maxDate = null;
11982
+ for (const task of tasks) {
11983
+ const start = parseUTCDate(task.startDate);
11984
+ const end = parseUTCDate(task.endDate);
11985
+ if (!minDate || start.getTime() < minDate.getTime()) {
11986
+ minDate = start;
11987
+ }
11988
+ if (!maxDate || end.getTime() > maxDate.getTime()) {
11989
+ maxDate = end;
11990
+ }
11991
+ }
11992
+ if (!minDate || !maxDate) {
11993
+ return getMultiMonthDays(tasks);
11994
+ }
11995
+ const startOfMonth2 = new Date(Date.UTC(minDate.getUTCFullYear(), minDate.getUTCMonth(), 1));
11996
+ const endOfMonth = new Date(Date.UTC(maxDate.getUTCFullYear(), maxDate.getUTCMonth() + 1, 0));
11997
+ const days = [];
11998
+ const current = new Date(startOfMonth2);
11999
+ while (current.getTime() <= endOfMonth.getTime()) {
12000
+ days.push(new Date(Date.UTC(
12001
+ current.getUTCFullYear(),
12002
+ current.getUTCMonth(),
12003
+ current.getUTCDate()
12004
+ )));
12005
+ current.setUTCDate(current.getUTCDate() + 1);
12006
+ }
12007
+ return days;
12008
+ }
12009
+ function getPlanFactRangeTasks(tasks) {
12010
+ const rangeTasks = [];
12011
+ for (const task of tasks) {
12012
+ rangeTasks.push({ startDate: task.startDate, endDate: task.endDate });
12013
+ for (const dateKey of Object.keys(task.planByDate ?? {})) {
12014
+ rangeTasks.push({ startDate: dateKey, endDate: dateKey });
12015
+ }
12016
+ for (const dateKey of Object.keys(task.factByDate ?? {})) {
12017
+ rangeTasks.push({ startDate: dateKey, endDate: dateKey });
12018
+ }
12019
+ }
12020
+ return rangeTasks;
12021
+ }
12022
+ function clampScrollValue(value, max) {
12023
+ return Math.min(max, Math.max(0, value));
12024
+ }
11797
12025
  function arePositionMapsEqual(left, right) {
11798
12026
  if (left.size !== right.size) return false;
11799
12027
  for (const [taskId, leftPosition] of left) {
@@ -11916,6 +12144,7 @@ function TaskGanttChartInner(props, ref) {
11916
12144
  const [taskListHasRightShadow, setTaskListHasRightShadow] = (0, import_react18.useState)(false);
11917
12145
  const [internalTaskDateChangeMode, setInternalTaskDateChangeMode] = (0, import_react18.useState)("preserve-duration");
11918
12146
  const [scrollViewport, setScrollViewport] = (0, import_react18.useState)({ scrollTop: 0, viewportHeight: 0 });
12147
+ const [planFactDateWindow, setPlanFactDateWindow] = (0, import_react18.useState)(null);
11919
12148
  const [selectedChip, setSelectedChip] = (0, import_react18.useState)(null);
11920
12149
  const [activeTimelineTooltip, setActiveTimelineTooltip] = (0, import_react18.useState)(null);
11921
12150
  const [internalCollapsedParentIds, setInternalCollapsedParentIds] = (0, import_react18.useState)(/* @__PURE__ */ new Set());
@@ -11934,6 +12163,9 @@ function TaskGanttChartInner(props, ref) {
11934
12163
  [customDays, isWeekend3]
11935
12164
  );
11936
12165
  const dateRangeTasks = (0, import_react18.useMemo)(() => {
12166
+ if (isPlanFactMode) {
12167
+ return getPlanFactRangeTasks(normalizedTasks);
12168
+ }
11937
12169
  if (!showBaseline) {
11938
12170
  return normalizedTasks;
11939
12171
  }
@@ -11942,8 +12174,11 @@ function TaskGanttChartInner(props, ref) {
11942
12174
  startDate: task.baselineStartDate && parseUTCDate(task.baselineStartDate).getTime() < parseUTCDate(task.startDate).getTime() ? task.baselineStartDate : task.startDate,
11943
12175
  endDate: task.baselineEndDate && parseUTCDate(task.baselineEndDate).getTime() > parseUTCDate(task.endDate).getTime() ? task.baselineEndDate : task.endDate
11944
12176
  }));
11945
- }, [normalizedTasks, showBaseline]);
11946
- const dateRange = (0, import_react18.useMemo)(() => getMultiMonthDays(dateRangeTasks), [dateRangeTasks]);
12177
+ }, [isPlanFactMode, normalizedTasks, showBaseline]);
12178
+ const dateRange = (0, import_react18.useMemo)(
12179
+ () => isPlanFactMode ? getFullMonthDays(dateRangeTasks) : getMultiMonthDays(dateRangeTasks),
12180
+ [dateRangeTasks, isPlanFactMode]
12181
+ );
11947
12182
  const [validationResult, setValidationResult] = (0, import_react18.useState)(null);
11948
12183
  const [cascadeOverrides, setCascadeOverrides] = (0, import_react18.useState)(/* @__PURE__ */ new Map());
11949
12184
  const gridWidth = (0, import_react18.useMemo)(
@@ -12009,11 +12244,12 @@ function TaskGanttChartInner(props, ref) {
12009
12244
  const firstDay = dateRange[0];
12010
12245
  return new Date(Date.UTC(firstDay.getUTCFullYear(), firstDay.getUTCMonth(), 1));
12011
12246
  }, [dateRange]);
12012
- const todayInRange = (0, import_react18.useMemo)(() => {
12247
+ const todayIndex = (0, import_react18.useMemo)(() => {
12013
12248
  const now = /* @__PURE__ */ new Date();
12014
12249
  const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
12015
- return dateRange.some((day) => day.getTime() === today.getTime());
12250
+ return dateRange.findIndex((day) => day.getTime() === today.getTime());
12016
12251
  }, [dateRange]);
12252
+ const todayInRange = todayIndex !== -1;
12017
12253
  const visibleTimelineMarkers = (0, import_react18.useMemo)(() => {
12018
12254
  if (isTableMatrixMode || !timelineMarkers || timelineMarkers.length === 0 || dateRange.length === 0) {
12019
12255
  return [];
@@ -12032,9 +12268,9 @@ function TaskGanttChartInner(props, ref) {
12032
12268
  if (!container || dateRange.length === 0) return;
12033
12269
  const now = /* @__PURE__ */ new Date();
12034
12270
  const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
12035
- const todayIndex = dateRange.findIndex((day) => day.getTime() === today.getTime());
12036
- if (todayIndex === -1) return;
12037
- const todayOffset = todayIndex * dayWidth;
12271
+ const todayIndex2 = dateRange.findIndex((day) => day.getTime() === today.getTime());
12272
+ if (todayIndex2 === -1) return;
12273
+ const todayOffset = todayIndex2 * dayWidth;
12038
12274
  const containerWidth = container.clientWidth;
12039
12275
  const scrollLeft = Math.round(todayOffset + dayWidth / 2 - containerWidth * 0.3);
12040
12276
  container.scrollLeft = Math.max(0, scrollLeft);
@@ -12048,13 +12284,33 @@ function TaskGanttChartInner(props, ref) {
12048
12284
  frameId = null;
12049
12285
  const nextHasRightShadow = container.scrollLeft > 0;
12050
12286
  const nextViewportHeight = Math.max(0, container.clientHeight - timelineHeaderHeight);
12287
+ const nextViewportWidth = Math.max(0, container.clientWidth - (showTaskList ? taskListWidth : 0));
12051
12288
  const nextScrollTop = container.scrollTop;
12289
+ const nextScrollLeft = container.scrollLeft;
12290
+ const nextChartScrollLeft = Math.max(0, nextScrollLeft);
12052
12291
  setTaskListHasRightShadow(
12053
12292
  (previous) => previous === nextHasRightShadow ? previous : nextHasRightShadow
12054
12293
  );
12055
12294
  setScrollViewport(
12056
12295
  (previous) => previous.scrollTop === nextScrollTop && previous.viewportHeight === nextViewportHeight ? previous : { scrollTop: nextScrollTop, viewportHeight: nextViewportHeight }
12057
12296
  );
12297
+ setPlanFactDateWindow((previous) => {
12298
+ if (!isPlanFactMode || dateRange.length === 0 || nextViewportWidth <= 0) {
12299
+ return previous === null ? previous : null;
12300
+ }
12301
+ const firstVisibleColumn = Math.max(0, Math.floor(nextChartScrollLeft / dayWidth));
12302
+ const visibleColumnCount = Math.max(1, Math.ceil(nextViewportWidth / dayWidth));
12303
+ const lastVisibleColumn = Math.min(dateRange.length - 1, firstVisibleColumn + visibleColumnCount - 1);
12304
+ const rangeStart = Math.max(
12305
+ 0,
12306
+ Math.floor(Math.max(0, firstVisibleColumn - PLAN_FACT_COLUMN_OVERSCAN) / PLAN_FACT_COLUMN_WINDOW_STEP) * PLAN_FACT_COLUMN_WINDOW_STEP
12307
+ );
12308
+ const rangeEnd = Math.min(
12309
+ dateRange.length - 1,
12310
+ Math.ceil((lastVisibleColumn + PLAN_FACT_COLUMN_OVERSCAN + 1) / PLAN_FACT_COLUMN_WINDOW_STEP) * PLAN_FACT_COLUMN_WINDOW_STEP - 1
12311
+ );
12312
+ return previous?.start === rangeStart && previous.end === rangeEnd ? previous : { start: rangeStart, end: rangeEnd };
12313
+ });
12058
12314
  };
12059
12315
  const scheduleUpdate = () => {
12060
12316
  if (frameId !== null) return;
@@ -12075,16 +12331,16 @@ function TaskGanttChartInner(props, ref) {
12075
12331
  container.removeEventListener("scroll", scheduleUpdate);
12076
12332
  window.removeEventListener("resize", scheduleUpdate);
12077
12333
  };
12078
- }, [timelineHeaderHeight]);
12334
+ }, [dateRange.length, dayWidth, isPlanFactMode, showTaskList, taskListWidth, timelineHeaderHeight]);
12079
12335
  const scrollToToday = (0, import_react18.useCallback)(() => {
12080
12336
  if (isTableMatrixMode) return;
12081
12337
  const container = scrollContainerRef.current;
12082
12338
  if (!container || dateRange.length === 0) return;
12083
12339
  const now = /* @__PURE__ */ new Date();
12084
12340
  const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
12085
- const todayIndex = dateRange.findIndex((day) => day.getTime() === today.getTime());
12086
- if (todayIndex === -1) return;
12087
- const todayOffset = todayIndex * dayWidth;
12341
+ const todayIndex2 = dateRange.findIndex((day) => day.getTime() === today.getTime());
12342
+ if (todayIndex2 === -1) return;
12343
+ const todayOffset = todayIndex2 * dayWidth;
12088
12344
  const containerWidth = container.clientWidth;
12089
12345
  const scrollLeft = Math.round(todayOffset + dayWidth / 2 - containerWidth * 0.3);
12090
12346
  container.scrollTo({ left: Math.max(0, scrollLeft), behavior: "smooth" });
@@ -12320,6 +12576,15 @@ function TaskGanttChartInner(props, ref) {
12320
12576
  }
12321
12577
  return Array.from(indices).sort((left, right) => left - right);
12322
12578
  }, [effectiveRowHeight, forcedRenderedTaskIds, scrollViewport, visibleTaskIndexMap, visibleTasks.length]);
12579
+ const visiblePlanFactDateIndices = (0, import_react18.useMemo)(() => {
12580
+ if (!isPlanFactMode || dateRange.length === 0 || !planFactDateWindow) {
12581
+ return void 0;
12582
+ }
12583
+ return Array.from(
12584
+ { length: planFactDateWindow.end - planFactDateWindow.start + 1 },
12585
+ (_, index) => planFactDateWindow.start + index
12586
+ );
12587
+ }, [dateRange.length, isPlanFactMode, planFactDateWindow]);
12323
12588
  const renderedChartTasks = (0, import_react18.useMemo)(
12324
12589
  () => visibleTaskWindowIndices.map((index) => {
12325
12590
  const task = previewVisibleTasks[index];
@@ -12576,7 +12841,10 @@ function TaskGanttChartInner(props, ref) {
12576
12841
  startX: e.clientX,
12577
12842
  startY: e.clientY,
12578
12843
  scrollX: container.scrollLeft,
12579
- scrollY: container.scrollTop
12844
+ scrollY: container.scrollTop,
12845
+ currentX: e.clientX,
12846
+ currentY: e.clientY,
12847
+ frameId: null
12580
12848
  };
12581
12849
  if (document.activeElement instanceof HTMLElement) {
12582
12850
  document.activeElement.blur();
@@ -12585,16 +12853,37 @@ function TaskGanttChartInner(props, ref) {
12585
12853
  e.preventDefault();
12586
12854
  }, []);
12587
12855
  (0, import_react18.useEffect)(() => {
12588
- const handlePanMove = (e) => {
12856
+ const flushPanMove = () => {
12589
12857
  const pan = panStateRef.current;
12590
12858
  if (!pan?.active) return;
12859
+ pan.frameId = null;
12591
12860
  const container = scrollContainerRef.current;
12592
12861
  if (!container) return;
12593
- container.scrollLeft = pan.scrollX - (e.clientX - pan.startX);
12594
- container.scrollTop = pan.scrollY - (e.clientY - pan.startY);
12862
+ const maxScrollLeft = Math.max(0, container.scrollWidth - container.clientWidth);
12863
+ const maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
12864
+ const nextScrollLeft = clampScrollValue(pan.scrollX - (pan.currentX - pan.startX), maxScrollLeft);
12865
+ const nextScrollTop = clampScrollValue(pan.scrollY - (pan.currentY - pan.startY), maxScrollTop);
12866
+ if (Math.abs(container.scrollLeft - nextScrollLeft) > 0.5) {
12867
+ container.scrollLeft = nextScrollLeft;
12868
+ }
12869
+ if (Math.abs(container.scrollTop - nextScrollTop) > 0.5) {
12870
+ container.scrollTop = nextScrollTop;
12871
+ }
12872
+ };
12873
+ const handlePanMove = (e) => {
12874
+ const pan = panStateRef.current;
12875
+ if (!pan?.active) return;
12876
+ pan.currentX = e.clientX;
12877
+ pan.currentY = e.clientY;
12878
+ if (pan.frameId !== null) return;
12879
+ pan.frameId = window.requestAnimationFrame(flushPanMove);
12595
12880
  };
12596
12881
  const handlePanEnd = () => {
12597
- if (!panStateRef.current?.active) return;
12882
+ const pan = panStateRef.current;
12883
+ if (!pan?.active) return;
12884
+ if (pan.frameId !== null) {
12885
+ window.cancelAnimationFrame(pan.frameId);
12886
+ }
12598
12887
  panStateRef.current = null;
12599
12888
  const container = scrollContainerRef.current;
12600
12889
  if (container) container.style.cursor = "";
@@ -12721,7 +13010,10 @@ function TaskGanttChartInner(props, ref) {
12721
13010
  onTasksChange: handleTaskChange,
12722
13011
  onCellCommit: onPlanFactCellCommit,
12723
13012
  highlightedTaskIds: taskListHighlightedTaskIds,
12724
- filterMode
13013
+ filterMode,
13014
+ visibleRowIndices: visibleTaskWindowIndices,
13015
+ visibleDateIndices: visiblePlanFactDateIndices,
13016
+ todayDateIndex: todayInRange ? todayIndex : void 0
12725
13017
  }
12726
13018
  ) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
12727
13019
  /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(