Skip to content

Commit 841f448

Browse files
committed
add cellspan possibilty
1 parent 7c0c833 commit 841f448

File tree

12 files changed

+160
-28
lines changed

12 files changed

+160
-28
lines changed

src/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ReactGrid } from './lib/Components/ReactGrid';
55
// import './test/theming-test.scss';
66
import {
77
config, enablePinnedToBodyConfig, disabledInitialFocusLocationConfig, enableAdditionalContentConfig,
8-
enableAdditionalContentWithFlexRowConfig, enableSymetric, enableResponsiveSticky, enableResponsiveStickyPinnedToBody
8+
enableAdditionalContentWithFlexRowConfig, enableSymetric, enableResponsiveSticky, enableResponsiveStickyPinnedToBody, enableSpannedCells
99
} from './test/testEnvConfig';
1010

1111
let component = <ExtTestGrid
@@ -87,6 +87,14 @@ switch (window.location.pathname) {
8787
/>;
8888
ExtTestGrid.displayName = 'TestGridWithResponsiveStickyPinnedToBody';
8989
break;
90+
case '/enableSpannedCells':
91+
component = <ExtTestGrid
92+
component={ReactGrid}
93+
config={enableSpannedCells}
94+
cellType={'header'}
95+
/>;
96+
ExtTestGrid.displayName = 'TestGridWithSpannedCells';
97+
break;
9098
default:
9199
break;
92100
}

src/lib/Components/CellFocus.tsx

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,50 @@
11
import * as React from 'react';
22
import { Location } from '../Model/InternalModel';
3+
import { State } from '../Model/State';
34

45
interface CellFocusProps {
6+
state?: State;
57
location: Location;
68
isHighlight?: boolean;
79
borderColor?: string;
810
className?: string;
911
}
12+
export const Highlight: React.FC<CellFocusProps> = ({ borderColor, isHighlight, location, className, state }) => {
13+
const colIdx = location.column.idx;
14+
const rowIdx = location.row.idx;
15+
const range = state?.cellMatrix.rangesToRender[state.cellMatrix.getLocationToFindRangeByIds(colIdx, rowIdx)]?.range;
16+
if (!range) {
17+
return null;
18+
}
19+
return (
20+
<div
21+
key={borderColor}
22+
className={`${isHighlight ? 'rg-cell-highlight' : 'rg-cell-focus'} ${className || ''}`}
23+
style={{
24+
top: location.row.top - (location.row.top === 0 ? 0 : 1),
25+
left: location.column.left - (location.column.left === 0 ? 0 : 1),
26+
width: range.width + (location.column.left === 0 ? 0 : 1),
27+
height: range.height + (location.row.top === 0 ? 0 : 1),
28+
borderColor: `${borderColor}`,
29+
}}
30+
/>
31+
);
32+
}
1033

1134
// TODO make HOC for highlights
12-
export const CellFocus: React.FC<CellFocusProps> = ({ borderColor, isHighlight, location, className }) => (
13-
<div
14-
key={borderColor}
15-
className={`${isHighlight ? 'rg-cell-highlight' : 'rg-cell-focus'} ${className || ''}`}
16-
style={{
17-
top: location.row.top - (location.row.top === 0 ? 0 : 1),
18-
left: location.column.left - (location.column.left === 0 ? 0 : 1),
19-
width: location.column.width + (location.column.left === 0 ? 0 : 1),
20-
height: location.row.height + (location.row.top === 0 ? 0 : 1),
21-
borderColor: `${borderColor}`,
22-
}}
23-
/>
24-
);
35+
export const CellFocus: React.FC<CellFocusProps> = ({ borderColor, isHighlight, location, className }) => {
36+
37+
return (
38+
<div
39+
key={borderColor}
40+
className={`${isHighlight ? 'rg-cell-highlight' : 'rg-cell-focus'} ${className || ''}`}
41+
style={{
42+
top: location.row.top - (location.row.top === 0 ? 0 : 1),
43+
left: location.column.left - (location.column.left === 0 ? 0 : 1),
44+
width: location.column.width + (location.column.left === 0 ? 0 : 1),
45+
height: location.row.height + (location.row.top === 0 ? 0 : 1),
46+
borderColor: `${borderColor}`,
47+
}}
48+
/>
49+
);
50+
}

src/lib/Components/CellRenderer.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { noBorder } from '../Functions/excludeObjectProperties';
44
import { getCompatibleCellAndTemplate } from '../Functions/getCompatibleCellAndTemplate';
55
import { tryAppendChange } from '../Functions/tryAppendChange';
66
import { Borders, Location } from '../Model/InternalModel';
7+
import { Range } from '../Model/Range'
78
import { BorderProps, Cell, Compatible } from '../Model/PublicModel';
89
import { State } from '../Model/State';
910
import { isMobileDevice } from '../Functions/isMobileDevice';
@@ -12,6 +13,7 @@ export interface CellRendererProps {
1213
state: State;
1314
location: Location;
1415
borders: Borders;
16+
range: Range;
1517
children?: React.ReactNode;
1618
}
1719

@@ -44,7 +46,7 @@ function getBorderProperties(getPropertyOnBorderFn: (borderEdge: keyof Borders)
4446
}
4547
}
4648

47-
export const CellRenderer: React.FC<CellRendererProps> = ({ state, location, children, borders }) => {
49+
export const CellRenderer: React.FC<CellRendererProps> = ({ state, location, range, children, borders }) => {
4850
const { cell, cellTemplate } = getCompatibleCellAndTemplate(state, location);
4951
const isFocused = state.focusedLocation !== undefined && areLocationsEqual(state.focusedLocation, location);
5052
const customClass = (cellTemplate.getClassName && cellTemplate.getClassName(cell, false)) ?? '';
@@ -67,14 +69,13 @@ export const CellRenderer: React.FC<CellRendererProps> = ({ state, location, chi
6769
const isMobile = isMobileDevice();
6870
const isFirstRowOrColumnWithSelection = (state.props?.enableRowSelection && location.row.idx === 0)
6971
|| (state.props?.enableColumnSelection && location.column.idx === 0);
70-
7172
const style = {
7273
...(cellTemplate.getStyle && (cellTemplate.getStyle(cell, false) || {})),
7374
...(cell.style && noBorder(cell.style)),
7475
left: location.column.left,
7576
top: location.row.top,
76-
width: location.column.width,
77-
height: location.row.height,
77+
width: range.width,
78+
height: range.height,
7879
...(!(isFocused && state.currentlyEditedCell) && bordersProps),
7980
...((isFocused || cell.type === 'header' || isFirstRowOrColumnWithSelection) && { touchAction: 'none' }) // prevent scrolling
8081
} as React.CSSProperties;

src/lib/Components/Pane.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Range } from '../Model/Range';
33
import { State } from '../Model/State';
44
import { Borders } from '../Model/InternalModel';
55
import { Highlight } from '../Model/PublicModel';
6-
import { CellFocus } from './CellFocus';
6+
import { CellFocus, Highlight as CellHighlight } from './CellFocus';
77
import { RowRenderer } from './RowRenderer';
88
import { CellRendererProps } from './CellRenderer';
99
import { isMobileDevice } from '../Functions/isMobileDevice';
@@ -62,7 +62,7 @@ function renderHighlights(state: State, range: Range) {
6262
try {
6363
const location = state.cellMatrix.getLocationById(highlight.rowId, highlight.columnId);
6464
return location && range.contains(location) &&
65-
<CellFocus key={id} location={location} borderColor={highlight.borderColor} isHighlight />;
65+
<CellHighlight key={id} location={location} state={state} borderColor={highlight.borderColor} isHighlight />;
6666
} catch (error) {
6767
console.error(`Cell location fot found while rendering highlights at: ${(error as Error).message}`);
6868
return null;

src/lib/Components/ReactGrid.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class ReactGrid extends React.Component<ReactGridProps, State> {
2626
update: this.stateUpdater,
2727
cellMatrix: this.cellMatrixBuilder.setProps(this.props)
2828
.fillRowsAndCols()
29+
.setRangesToRenderLookup()
2930
.fillSticky()
3031
.fillScrollableRange()
3132
.setEdgeLocations()

src/lib/Components/RowRenderer.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ const MappedColumns: React.FC<RowRendererProps> = ({ columns, row, cellRenderer,
2727
<>
2828
{columns.map(column => {
2929
const location: Location = { row, column };
30+
const range = state.cellMatrix.rangesToRender[state.cellMatrix.getLocationToFindRangeByIds(column.idx, row.idx)]?.range;
31+
if (!range) {
32+
return null;
33+
}
3034
return <CellRenderer
3135
key={row.idx + '-' + column.idx}
3236
borders={{
@@ -35,7 +39,9 @@ const MappedColumns: React.FC<RowRendererProps> = ({ columns, row, cellRenderer,
3539
right: (borders.right && column.idx === lastColIdx) || !(state.cellMatrix.scrollableRange.last.column.idx === location.column.idx)
3640
}}
3741
state={state}
38-
location={location} />
42+
location={location}
43+
range={range}
44+
/>
3945
})}
4046
</>
4147
);

src/lib/Functions/getDerivedStateFromProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ function updateCellMatrix(props: ReactGridProps, state: State): State {
6969
...state,
7070
cellMatrix: builder.setProps(props)
7171
.fillRowsAndCols({ leftStickyColumns: state.leftStickyColumns || 0, topStickyRows: state.topStickyRows || 0 })
72+
.setRangesToRenderLookup()
7273
.fillSticky({ leftStickyColumns: state.leftStickyColumns || 0, topStickyRows: state.topStickyRows || 0 })
7374
.fillScrollableRange({ leftStickyColumns: state.leftStickyColumns || 0, topStickyRows: state.topStickyRows || 0 })
7475
.setEdgeLocations().getCellMatrix()

src/lib/Model/CellMatrix.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export interface StickyRanges {
2020
stickyLeftRange: Range;
2121
}
2222

23+
export interface Span {
24+
range?: Range;
25+
}
26+
2327
// INTERNAL
2428
export class CellMatrix<TStickyRanges extends StickyRanges = StickyRanges, TCellMatrixProps extends CellMatrixProps = CellMatrixProps> {
2529

@@ -40,6 +44,10 @@ export class CellMatrix<TStickyRanges extends StickyRanges = StickyRanges, TCell
4044
rowIndexLookup: IndexLookup = {};
4145
columnIndexLookup: IndexLookup = {};
4246

47+
spanCellLookup: { [location: string]: Span } = {};
48+
49+
rangesToRender: { [location: string]: Span } = {};
50+
4351
constructor(public ranges: TStickyRanges) { }
4452

4553
getRange(start: Location, end: Location): Range {
@@ -68,6 +76,10 @@ export class CellMatrix<TStickyRanges extends StickyRanges = StickyRanges, TCell
6876
return this.getLocation(rowIdx, colIdx);
6977
}
7078

79+
getLocationToFindRangeByIds(idx: number, idy: number): string {
80+
return `${idx}, ${idy}`;
81+
}
82+
7183
validateRange(range: Range): Range {
7284
return this.getRange(this.validateLocation(range.first), this.validateLocation(range.last));
7385
}

src/lib/Model/CellMatrixBuilder.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CellMatrix, CellMatrixProps, StickyRanges } from './CellMatrix';
1+
import { CellMatrix, CellMatrixProps, StickyRanges, Span } from './CellMatrix';
22
import { GridColumn, GridRow } from './InternalModel';
33
import { Range } from './Range';
44

@@ -41,19 +41,18 @@ export class CellMatrixBuilder implements ICellMatrixBuilder {
4141
if (!Array.isArray(this.cellMatrix.props.columns)) {
4242
throw new Error('Feeded ReactGrids "columns" property is not an array!')
4343
}
44-
this.cellMatrix.rows = this.cellMatrix.props.rows.reduce(
44+
this.cellMatrix.rows = this.cellMatrix.props.rows.reduce<GridRow[]>(
4545
(rows, row, idx) => {
4646
const top = this.getTop(idx, topStickyRows, rows);
4747
const height = row.height || CellMatrix.DEFAULT_ROW_HEIGHT;
4848
rows.push({ ...row, top, height, idx, bottom: top + height } as GridRow);
4949
this.cellMatrix.height += height;
50-
// TODO what with rowIndexLookup?
5150
this.cellMatrix.rowIndexLookup[row.rowId] = idx;
5251
return rows;
5352
},
54-
[] as GridRow[]
53+
[]
5554
);
56-
this.cellMatrix.columns = this.cellMatrix.props.columns.reduce(
55+
this.cellMatrix.columns = this.cellMatrix.props.columns.reduce<GridColumn[]>(
5756
(cols, column, idx) => {
5857
const left = this.getLeft(idx, leftStickyColumns, cols);
5958
const width = column.width
@@ -65,11 +64,36 @@ export class CellMatrixBuilder implements ICellMatrixBuilder {
6564
this.cellMatrix.columnIndexLookup[column.columnId] = idx;
6665
return cols;
6766
},
68-
[] as GridColumn[]
67+
[]
6968
);
7069
return this;
7170
}
7271

72+
setRangesToRenderLookup(): CellMatrixBuilder {
73+
let rangesToExclude: Range[] = [];
74+
this.cellMatrix.rows.forEach((row, idy) => {
75+
row.cells.forEach((cell, idx) => {
76+
const { rowspan = 0, colspan = 0 } = cell;
77+
const rows = rowspan ? this.cellMatrix.rows.slice(idy, idy + rowspan) : [this.cellMatrix.rows[idy]];
78+
const columns = colspan ? this.cellMatrix.columns.slice(idx, idx + colspan) : [this.cellMatrix.columns[idx]];
79+
const range = new Range(rows, columns)
80+
rangesToExclude = rangesToExclude.concat(this.getRangesToRender(range));
81+
this.cellMatrix.spanCellLookup[this.cellMatrix.getLocationToFindRangeByIds(idx, idy)] = {
82+
range: range
83+
}
84+
})
85+
});
86+
const keys = rangesToExclude.map(range => `${range.columns[0].idx}, ${range.rows[0].idx}`);
87+
Object.keys(this.cellMatrix.spanCellLookup).filter(key => !keys.includes(key)).forEach(key => this.cellMatrix.rangesToRender[key] = this.cellMatrix.spanCellLookup[key]);
88+
return this;
89+
}
90+
91+
getRangesToRender(range: Range): Range[] {
92+
const result = range.rows.flatMap(row => range.columns.map(column => new Range([row], [column])));
93+
result.shift();
94+
return result;
95+
}
96+
7397
fillSticky(edges: StickyEdges = { leftStickyColumns: 0, topStickyRows: 0 }): CellMatrixBuilder {
7498
const { leftStickyColumns, topStickyRows } = edges;
7599
this.cellMatrix.ranges.stickyLeftRange = new Range(this.cellMatrix.rows,

src/lib/Model/PublicModel.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,10 @@ export interface Cell {
460460
style?: CellStyle;
461461
/** Additional CSS classes */
462462
className?: string;
463+
/** Specifies the number of columns a cell should span */
464+
colspan?: number;
465+
/** Specifies the number of rows a cell should span */
466+
rowspan?: number;
463467
}
464468

465469
/**

0 commit comments

Comments
 (0)