@xcelsior/ui-spreadsheets 1.1.9 → 1.1.11
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 +174 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +174 -19
- package/dist/index.mjs.map +1 -1
- package/dist/styles/globals.css +3 -0
- package/dist/styles/globals.css.map +1 -1
- package/package.json +1 -1
- package/src/components/Spreadsheet.stories.tsx +223 -17
- package/src/components/Spreadsheet.tsx +238 -23
|
@@ -26,7 +26,7 @@ import { useSpreadsheetComments } from '../hooks/useSpreadsheetComments';
|
|
|
26
26
|
import { useSpreadsheetUndoRedo } from '../hooks/useSpreadsheetUndoRedo';
|
|
27
27
|
import { useSpreadsheetKeyboardShortcuts } from '../hooks/useSpreadsheetKeyboardShortcuts';
|
|
28
28
|
import { useSpreadsheetSelection } from '../hooks/useSpreadsheetSelection';
|
|
29
|
-
import type { CellEdit, SpreadsheetProps } from '../types';
|
|
29
|
+
import type { CellEdit, SpreadsheetColumn, SpreadsheetColumnGroup, SpreadsheetProps } from '../types';
|
|
30
30
|
|
|
31
31
|
type SingleCellEdit = {
|
|
32
32
|
rowId: string | number;
|
|
@@ -634,6 +634,172 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
634
634
|
setHighlightPickerColumn(ROW_INDEX_COLUMN_ID);
|
|
635
635
|
}, [setHighlightPickerColumn]);
|
|
636
636
|
|
|
637
|
+
// Build render items that include placeholder cells for collapsed groups
|
|
638
|
+
// Pinned columns are moved to the edges: left-pinned first, then unpinned, then right-pinned
|
|
639
|
+
const columnRenderItems = useMemo(() => {
|
|
640
|
+
if (!columnGroups || columnGroups.length === 0) {
|
|
641
|
+
return visibleColumns.map((col) => ({
|
|
642
|
+
type: 'column' as const,
|
|
643
|
+
column: col,
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
type ColumnItem = { type: 'column'; column: SpreadsheetColumn<T> };
|
|
648
|
+
type PlaceholderItem = { type: 'collapsed-placeholder'; groupId: string; headerColor?: string };
|
|
649
|
+
type RenderItem = ColumnItem | PlaceholderItem;
|
|
650
|
+
|
|
651
|
+
const leftPinnedItems: ColumnItem[] = [];
|
|
652
|
+
const middleItems: RenderItem[] = [];
|
|
653
|
+
const rightPinnedItems: ColumnItem[] = [];
|
|
654
|
+
|
|
655
|
+
for (const group of columnGroups) {
|
|
656
|
+
const isCollapsed = collapsedGroups.has(group.id);
|
|
657
|
+
|
|
658
|
+
if (isCollapsed) {
|
|
659
|
+
middleItems.push({
|
|
660
|
+
type: 'collapsed-placeholder',
|
|
661
|
+
groupId: group.id,
|
|
662
|
+
headerColor: group.headerColor,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Get columns from this group that are visible (preserving original order)
|
|
667
|
+
const groupVisibleCols = (columns || []).filter((c) => {
|
|
668
|
+
if (!group.columns.includes(c.id)) return false;
|
|
669
|
+
if (isCollapsed) return pinnedColumns.has(c.id);
|
|
670
|
+
return true;
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
for (const col of groupVisibleCols) {
|
|
674
|
+
const pinSide = pinnedColumns.get(col.id);
|
|
675
|
+
if (pinSide === 'left') {
|
|
676
|
+
leftPinnedItems.push({ type: 'column', column: col });
|
|
677
|
+
} else if (pinSide === 'right') {
|
|
678
|
+
rightPinnedItems.push({ type: 'column', column: col });
|
|
679
|
+
} else {
|
|
680
|
+
middleItems.push({ type: 'column', column: col });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Add any columns not in any group
|
|
686
|
+
const allGroupedIds = new Set(
|
|
687
|
+
columnGroups.flatMap((g) => g.columns)
|
|
688
|
+
);
|
|
689
|
+
for (const col of visibleColumns) {
|
|
690
|
+
if (!allGroupedIds.has(col.id)) {
|
|
691
|
+
const pinSide = pinnedColumns.get(col.id);
|
|
692
|
+
if (pinSide === 'left') {
|
|
693
|
+
leftPinnedItems.push({ type: 'column', column: col });
|
|
694
|
+
} else if (pinSide === 'right') {
|
|
695
|
+
rightPinnedItems.push({ type: 'column', column: col });
|
|
696
|
+
} else {
|
|
697
|
+
middleItems.push({ type: 'column', column: col });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Sort pinned items by their order in the pinnedColumns map
|
|
703
|
+
const pinnedLeftOrder = Array.from(pinnedColumns.entries())
|
|
704
|
+
.filter(([id, side]) => side === 'left' && id !== ROW_INDEX_COLUMN_ID)
|
|
705
|
+
.map(([id]) => id);
|
|
706
|
+
const pinnedRightOrder = Array.from(pinnedColumns.entries())
|
|
707
|
+
.filter(([, side]) => side === 'right')
|
|
708
|
+
.map(([id]) => id);
|
|
709
|
+
|
|
710
|
+
leftPinnedItems.sort(
|
|
711
|
+
(a, b) => pinnedLeftOrder.indexOf(a.column.id) - pinnedLeftOrder.indexOf(b.column.id)
|
|
712
|
+
);
|
|
713
|
+
rightPinnedItems.sort(
|
|
714
|
+
(a, b) => pinnedRightOrder.indexOf(a.column.id) - pinnedRightOrder.indexOf(b.column.id)
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
return [...leftPinnedItems, ...middleItems, ...rightPinnedItems];
|
|
718
|
+
}, [columnGroups, collapsedGroups, columns, pinnedColumns, visibleColumns]);
|
|
719
|
+
|
|
720
|
+
// Build group header items that account for pinned columns being moved to edges
|
|
721
|
+
const groupHeaderItems = useMemo(() => {
|
|
722
|
+
if (!columnGroups || columnGroups.length === 0) return null;
|
|
723
|
+
|
|
724
|
+
type PinnedGroupHeaderItem = {
|
|
725
|
+
type: 'pinned-column';
|
|
726
|
+
columnId: string;
|
|
727
|
+
headerColor?: string;
|
|
728
|
+
pinSide: 'left' | 'right';
|
|
729
|
+
};
|
|
730
|
+
type GroupHeaderItem = {
|
|
731
|
+
type: 'group';
|
|
732
|
+
group: SpreadsheetColumnGroup;
|
|
733
|
+
colSpan: number;
|
|
734
|
+
isCollapsed: boolean;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const leftPinned: PinnedGroupHeaderItem[] = [];
|
|
738
|
+
const groups: GroupHeaderItem[] = [];
|
|
739
|
+
const rightPinned: PinnedGroupHeaderItem[] = [];
|
|
740
|
+
|
|
741
|
+
for (const group of columnGroups) {
|
|
742
|
+
const isCollapsed = collapsedGroups.has(group.id);
|
|
743
|
+
const groupColumns = (columns || []).filter((c) => group.columns.includes(c.id));
|
|
744
|
+
const visibleGroupColumns = isCollapsed
|
|
745
|
+
? groupColumns.filter((c) => pinnedColumns.has(c.id))
|
|
746
|
+
: groupColumns;
|
|
747
|
+
|
|
748
|
+
let movedLeftCount = 0;
|
|
749
|
+
let movedRightCount = 0;
|
|
750
|
+
|
|
751
|
+
for (const col of visibleGroupColumns) {
|
|
752
|
+
const pinSide = pinnedColumns.get(col.id);
|
|
753
|
+
if (pinSide === 'left') {
|
|
754
|
+
movedLeftCount++;
|
|
755
|
+
leftPinned.push({
|
|
756
|
+
type: 'pinned-column',
|
|
757
|
+
columnId: col.id,
|
|
758
|
+
headerColor: group.headerColor,
|
|
759
|
+
pinSide: 'left',
|
|
760
|
+
});
|
|
761
|
+
} else if (pinSide === 'right') {
|
|
762
|
+
movedRightCount++;
|
|
763
|
+
rightPinned.push({
|
|
764
|
+
type: 'pinned-column',
|
|
765
|
+
columnId: col.id,
|
|
766
|
+
headerColor: group.headerColor,
|
|
767
|
+
pinSide: 'right',
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const remainingCols = visibleGroupColumns.length - movedLeftCount - movedRightCount;
|
|
773
|
+
const colSpan = remainingCols + (isCollapsed ? 1 : 0);
|
|
774
|
+
|
|
775
|
+
if (colSpan > 0) {
|
|
776
|
+
groups.push({
|
|
777
|
+
type: 'group',
|
|
778
|
+
group,
|
|
779
|
+
colSpan,
|
|
780
|
+
isCollapsed,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Sort pinned items by their order in the pinnedColumns map
|
|
786
|
+
const pinnedLeftOrder = Array.from(pinnedColumns.entries())
|
|
787
|
+
.filter(([id, side]) => side === 'left' && id !== ROW_INDEX_COLUMN_ID)
|
|
788
|
+
.map(([id]) => id);
|
|
789
|
+
const pinnedRightOrder = Array.from(pinnedColumns.entries())
|
|
790
|
+
.filter(([, side]) => side === 'right')
|
|
791
|
+
.map(([id]) => id);
|
|
792
|
+
|
|
793
|
+
leftPinned.sort(
|
|
794
|
+
(a, b) => pinnedLeftOrder.indexOf(a.columnId) - pinnedLeftOrder.indexOf(b.columnId)
|
|
795
|
+
);
|
|
796
|
+
rightPinned.sort(
|
|
797
|
+
(a, b) => pinnedRightOrder.indexOf(a.columnId) - pinnedRightOrder.indexOf(b.columnId)
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
return [...leftPinned, ...groups, ...rightPinned];
|
|
801
|
+
}, [columnGroups, collapsedGroups, columns, pinnedColumns]);
|
|
802
|
+
|
|
637
803
|
// ==================== RENDER ====================
|
|
638
804
|
|
|
639
805
|
return (
|
|
@@ -673,15 +839,13 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
673
839
|
<div ref={tableRef} className="flex-1 overflow-auto border border-gray-200 rounded">
|
|
674
840
|
<div
|
|
675
841
|
style={{
|
|
676
|
-
|
|
677
|
-
transformOrigin: 'top left',
|
|
678
|
-
width: `${100 / (zoom / 100)}%`,
|
|
842
|
+
zoom: zoom / 100,
|
|
679
843
|
}}
|
|
680
844
|
>
|
|
681
845
|
<table className="w-full border-separate border-spacing-0 text-xs select-none">
|
|
682
846
|
<thead>
|
|
683
847
|
{/* Column Group Headers */}
|
|
684
|
-
{columnGroups && (
|
|
848
|
+
{columnGroups && groupHeaderItems && (
|
|
685
849
|
<tr>
|
|
686
850
|
{/* Row index column header (rowSpan=2 for groups) */}
|
|
687
851
|
<RowIndexColumnHeader
|
|
@@ -693,19 +857,37 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
693
857
|
hasColumnGroups={true}
|
|
694
858
|
compactMode={effectiveCompactMode}
|
|
695
859
|
/>
|
|
696
|
-
{
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
860
|
+
{groupHeaderItems.map((item) => {
|
|
861
|
+
if (item.type === 'pinned-column') {
|
|
862
|
+
const col = columns.find(
|
|
863
|
+
(c) => c.id === item.columnId
|
|
864
|
+
);
|
|
865
|
+
const isPinnedLeft = item.pinSide === 'left';
|
|
866
|
+
return (
|
|
867
|
+
<th
|
|
868
|
+
key={`pinned-group-${item.columnId}`}
|
|
869
|
+
className={cn(
|
|
870
|
+
'border border-gray-200 px-2 py-1.5 text-center font-bold text-gray-700',
|
|
871
|
+
'z-30'
|
|
872
|
+
)}
|
|
873
|
+
style={{
|
|
874
|
+
backgroundColor:
|
|
875
|
+
item.headerColor ||
|
|
876
|
+
'rgb(243 244 246)',
|
|
877
|
+
position: 'sticky',
|
|
878
|
+
left: isPinnedLeft
|
|
879
|
+
? `${getColumnLeftOffset(item.columnId)}px`
|
|
880
|
+
: undefined,
|
|
881
|
+
right: !isPinnedLeft
|
|
882
|
+
? `${getColumnRightOffset(item.columnId)}px`
|
|
883
|
+
: undefined,
|
|
884
|
+
minWidth:
|
|
885
|
+
col?.minWidth || col?.width,
|
|
886
|
+
}}
|
|
887
|
+
/>
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
const { group, colSpan, isCollapsed } = item;
|
|
709
891
|
return (
|
|
710
892
|
<th
|
|
711
893
|
key={group.id}
|
|
@@ -753,7 +935,24 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
753
935
|
compactMode={effectiveCompactMode}
|
|
754
936
|
/>
|
|
755
937
|
)}
|
|
756
|
-
{
|
|
938
|
+
{columnRenderItems.map((item) => {
|
|
939
|
+
if (item.type === 'collapsed-placeholder') {
|
|
940
|
+
return (
|
|
941
|
+
<th
|
|
942
|
+
key={`${item.groupId}-placeholder`}
|
|
943
|
+
className="border border-gray-200 px-2 py-1 text-center text-gray-400"
|
|
944
|
+
style={{
|
|
945
|
+
backgroundColor:
|
|
946
|
+
item.headerColor ||
|
|
947
|
+
'rgb(243 244 246)',
|
|
948
|
+
minWidth: '30px',
|
|
949
|
+
}}
|
|
950
|
+
>
|
|
951
|
+
...
|
|
952
|
+
</th>
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
const column = item.column;
|
|
757
956
|
const isPinnedLeft =
|
|
758
957
|
isColumnPinned(column.id) &&
|
|
759
958
|
getColumnPinSide(column.id) === 'left';
|
|
@@ -812,7 +1011,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
812
1011
|
{isLoading ? (
|
|
813
1012
|
<tr>
|
|
814
1013
|
<td
|
|
815
|
-
colSpan={
|
|
1014
|
+
colSpan={columnRenderItems.length + 1}
|
|
816
1015
|
className="text-center py-8 text-gray-500"
|
|
817
1016
|
>
|
|
818
1017
|
<div className="flex items-center justify-center gap-2">
|
|
@@ -824,7 +1023,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
824
1023
|
) : paginatedData.length === 0 ? (
|
|
825
1024
|
<tr>
|
|
826
1025
|
<td
|
|
827
|
-
colSpan={
|
|
1026
|
+
colSpan={columnRenderItems.length + 1}
|
|
828
1027
|
className="text-center py-8 text-gray-500"
|
|
829
1028
|
>
|
|
830
1029
|
{emptyMessage}
|
|
@@ -1007,7 +1206,21 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1007
1206
|
</td>
|
|
1008
1207
|
|
|
1009
1208
|
{/* Data Cells */}
|
|
1010
|
-
{
|
|
1209
|
+
{columnRenderItems.map((item) => {
|
|
1210
|
+
if (item.type === 'collapsed-placeholder') {
|
|
1211
|
+
return (
|
|
1212
|
+
<td
|
|
1213
|
+
key={`${item.groupId}-placeholder`}
|
|
1214
|
+
className="border border-gray-200 px-2 py-1 text-center text-gray-300"
|
|
1215
|
+
style={{
|
|
1216
|
+
backgroundColor:
|
|
1217
|
+
item.headerColor ||
|
|
1218
|
+
'rgb(243 244 246)',
|
|
1219
|
+
}}
|
|
1220
|
+
/>
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
const column = item.column;
|
|
1011
1224
|
const value = column.getValue
|
|
1012
1225
|
? column.getValue(row)
|
|
1013
1226
|
: row[column.id];
|
|
@@ -1055,7 +1268,9 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1055
1268
|
isPinned={isColPinned}
|
|
1056
1269
|
pinSide={colPinSide}
|
|
1057
1270
|
leftOffset={getColumnLeftOffset(column.id)}
|
|
1058
|
-
rightOffset={getColumnRightOffset(
|
|
1271
|
+
rightOffset={getColumnRightOffset(
|
|
1272
|
+
column.id
|
|
1273
|
+
)}
|
|
1059
1274
|
onClick={(e) =>
|
|
1060
1275
|
handleCellClick(rowId, column.id, e)
|
|
1061
1276
|
}
|