Skip to content

Commit cdf0211

Browse files
Merge cells (tests pending) (#35)
1 parent 9d58f74 commit cdf0211

10 files changed

Lines changed: 162 additions & 15 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Able to render 100000+ input boxes in react, A quick solution for web based spre
2323
* __Blazing Fast Rendering: Handles large datasets efficiently (100000+ input boxes).__
2424
* __Comprehensive Calculation Engine: Supports complex formulas and calculations. (= 2 * A2 + (B2 * C4))__
2525
* __Rich Formatting Options: Customize cell appearance with bold, italic, underline, and more.__
26+
* __Merge cells.__
2627
* __Undo, Redo actions.__
2728
* __Calculations Support, value should starts with "="__
2829
* __Select Multiple cells.__

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-spread-sheet-excel",
3-
"version": "3.1.3",
3+
"version": "3.1.5",
44
"description": "A quick example of rendering large number of input boxes in table using React JS, React Spread-sheet (Excel sheet)",
55
"keywords": [
66
"React spreadsheet",

src/lib/list/cell.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import { useInView } from "react-intersection-observer";
33
import Input from "./input";
4+
import { store, useAppSelector } from "../store";
45
interface Prop {
56
i: number;
67
j: number;
@@ -14,10 +15,37 @@ const Cell = (props: Prop) => {
1415
rootMargin: "100px",
1516
});
1617

17-
return (
18-
<td ref={ref} className={`${!inView ? "pv-4 sheet-not-in-view-table" : ""}`}>
18+
const colSpan = useAppSelector(store, (state) => {
19+
let val = state.data[props.i][props.j];
20+
if (inView && val.colSpan && val.rowSpan) {
21+
return val.colSpan;
22+
}
23+
return 1;
24+
});
25+
26+
const rowSpan = useAppSelector(store, (state) => {
27+
let val = state.data[props.i][props.j];
28+
if (inView && val.colSpan && val.rowSpan) {
29+
return val.rowSpan;
30+
}
31+
return 1;
32+
});
33+
34+
const skip = useAppSelector(store, (state) => {
35+
return state.data[props.i][props.j].skip;
36+
});
37+
38+
return !skip ? (
39+
<td
40+
ref={ref}
41+
className={`${!inView ? "pv-4 sheet-not-in-view-table" : ""}`}
42+
colSpan={colSpan}
43+
rowSpan={rowSpan}
44+
>
1945
{inView ? <Input key={`${props.i}-${props.j}`} {...props} /> : " "}
2046
</td>
47+
) : (
48+
<></>
2149
);
2250
};
2351

src/lib/list/context-menu.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import React, { useCallback, useEffect, useState } from "react";
22
import Icons from "../svg/icons";
3-
import { addColumn, addRow, deleteColumn, deleteRow, updateInputTypes } from "../reducer";
3+
import {
4+
addColumn,
5+
addRow,
6+
deleteColumn,
7+
deleteRow,
8+
mergeCells,
9+
updateInputTypes,
10+
} from "../reducer";
411
import { store } from "../store";
512

613
interface ContextMenuProps {
@@ -47,6 +54,12 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
4754
onClose();
4855
};
4956

57+
const mergeCell = () => {
58+
dispatch(mergeCells);
59+
onChange && onChange();
60+
onClose();
61+
};
62+
5063
const closeOnOutsideClick = useCallback(
5164
(e: MouseEvent) => {
5265
const inside = (e.target as HTMLElement).closest(".sheet-context-menu");
@@ -91,6 +104,10 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
91104
Paste
92105
</div>
93106
<div className="sheet-context-menu-divider"></div>
107+
<div className="sheet-context-menu-item" role="menuitem" onClick={() => mergeCell()}>
108+
Merge cells
109+
</div>
110+
<div className="sheet-context-menu-divider"></div>
94111
<div
95112
className="sheet-context-menu-item sheet-context-menu-item-with-submenu"
96113
role="menuitem"

src/lib/list/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const List = (props: Props) => {
9292
const parentEl = parentDivRef.current;
9393
if (el && parentEl && parentEl?.scrollTop > el?.scrollHeight - 3200) {
9494
const nextVal = 300 + Math.round(parentEl?.scrollTop / 32);
95-
if (autoAddAdditionalRows && nextVal >= itemLength && itemLength < 2000) {
95+
if (autoAddAdditionalRows && nextVal >= itemLength && itemLength < 1500) {
9696
//Add additional rows
9797
dispatch(addRows, { payload: generateDummyContent(300, store.getState().data[0].length) });
9898
} else {

src/lib/list/input.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,23 +85,45 @@ const Input = (props: Prop) => {
8585
}
8686
};
8787

88+
const findNext = (
89+
i: number,
90+
j: number,
91+
dir: "up" | "down" | "left" | "right",
92+
): { i: number; j: number } => {
93+
if (
94+
document.getElementById(`${i}-${j}`) ||
95+
(dir === "up" && i === 0) ||
96+
(dir === "left" && j === 0) ||
97+
(dir === "down" && i === rowLength - 1) ||
98+
(dir === "right" && j === columnLength - 1)
99+
) {
100+
return { i, j };
101+
} else {
102+
return findNext(
103+
dir === "up" ? i - 1 : dir === "down" ? i + 1 : i,
104+
dir === "left" ? j - 1 : dir === "right" ? j + 1 : j,
105+
dir,
106+
);
107+
}
108+
};
109+
88110
const moveToNext = (e: KeyboardEvent<HTMLInputElement>) => {
89111
let newI, newJ;
90112
switch (e.code) {
91113
case "ArrowLeft":
92114
newI = i;
93-
newJ = j - 1;
115+
newJ = j > 0 ? findNext(i, j - 1, "left").j : j;
94116
break;
95117
case "ArrowUp":
96-
newI = i - 1;
118+
newI = i > 0 ? findNext(i - 1, j, "up").i : i;
97119
newJ = j;
98120
break;
99121
case "ArrowRight":
100122
newI = i;
101-
newJ = j + 1;
123+
newJ = j < columnLength - 1 ? findNext(i, j + 1, "right").j : j;
102124
break;
103125
case "ArrowDown":
104-
newI = i + 1;
126+
newI = i < rowLength - 1 ? findNext(i + 1, j, "down").i : i;
105127
newJ = j;
106128
break;
107129
}

src/lib/list/tools/tools.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useRef } from "react";
22
import Icons from "../../svg/icons";
33
import { store, useAppSelector } from "../../store";
4-
import { changeData, redo, undo } from "../../reducer";
4+
import { changeData, mergeCells, redo, undo } from "../../reducer";
55

66
let timer: string | number | NodeJS.Timeout | undefined;
77
const emptyObject = {};
@@ -34,8 +34,15 @@ const Tools = ({
3434
});
3535
const selectedItemVal = useAppSelector(
3636
store,
37-
(state) => state.data[state.selected?.[0]?.[0]]?.[state.selected?.[0]?.[1]]?.value || "",
37+
(state) => state.data[state.selected?.[0]?.[0]]?.[state.selected?.[0]?.[1]].value || "",
3838
);
39+
40+
const rowSpan = useAppSelector(
41+
store,
42+
(state) =>
43+
state.data[state.selected?.[0]?.[0]]?.[state.selected?.[0]?.[1]].rowSpan || undefined,
44+
);
45+
3946
const selectedFontSize = selectedStyles?.["fontSize"]
4047
? selectedStyles["fontSize"]?.split("px")?.[0]
4148
: "12";
@@ -229,6 +236,15 @@ const Tools = ({
229236
<Icons type="align-justify" />
230237
</button>
231238
</div>
239+
<div className="sheet-tools-text-style-container">
240+
<button
241+
className={rowSpan ? "text-style-btn-active" : ""}
242+
data-testid="merge"
243+
onClick={() => dispatch(mergeCells)}
244+
>
245+
<Icons type="merge" />
246+
</button>
247+
</div>
232248
</div>
233249
</div>
234250
);

src/lib/reducer.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export interface Data {
66
value: string;
77
styles?: { [key: string]: string };
88
type?: string;
9+
colSpan?: number;
10+
rowSpan?: number;
11+
skip?: boolean; //merged
912
}
1013

1114
interface Action {
@@ -299,7 +302,6 @@ const actions: DispatcherActions = {
299302
},
300303
addColumn(state, action) {
301304
state.redo = [];
302-
// state.undo.push(undo);
303305
const index = action.payload.right ? state.selected[0][1] + 1 : state.selected[0][1];
304306
const data = state.data;
305307
state.undo.push([{ i: index, j: 0, type: "add-column", data: { value: "" } }]);
@@ -331,6 +333,48 @@ const actions: DispatcherActions = {
331333
]);
332334
return state;
333335
},
336+
mergeCells(state) {
337+
if (state.selected.length > 0) {
338+
state.redo = [];
339+
const undo: Action[] = [];
340+
const cellForMerge = state.selected[0];
341+
const data = state.data;
342+
if (data[cellForMerge[0]][cellForMerge[1]].rowSpan) {
343+
for (
344+
let i = cellForMerge[0];
345+
i <= cellForMerge[0] + (data[cellForMerge[0]][cellForMerge[1]].rowSpan || 0);
346+
i++
347+
) {
348+
for (
349+
let j = cellForMerge[1];
350+
j <= cellForMerge[1] + (data[cellForMerge[0]][cellForMerge[1]].colSpan || 0);
351+
j++
352+
) {
353+
undo.push({ i: i, j: j, data: { ...state.data[i][j] } });
354+
data[i][j].skip = undefined;
355+
}
356+
}
357+
data[cellForMerge[0]][cellForMerge[1]].rowSpan = undefined;
358+
data[cellForMerge[0]][cellForMerge[1]].colSpan = undefined;
359+
} else if (state.selected.length > 1) {
360+
state.selected.forEach((p, i) => {
361+
undo.push({ i: p[0], j: p[1], data: { ...state.data[p[0]][p[1]] } });
362+
if (i !== 0) {
363+
data[p[0]][p[1]].value = "";
364+
data[p[0]][p[1]].skip = true;
365+
}
366+
});
367+
data[cellForMerge[0]][cellForMerge[1]].rowSpan =
368+
Math.abs(state.selected[0][0] - state.selected[state.selected.length - 1][0]) + 1;
369+
data[cellForMerge[0]][cellForMerge[1]].colSpan =
370+
Math.abs(state.selected[0][1] - state.selected[state.selected.length - 1][1]) + 1;
371+
}
372+
state.selected = [cellForMerge];
373+
state.data = data;
374+
state.undo.push(undo);
375+
}
376+
return state;
377+
},
334378
};
335379

336380
export const {
@@ -354,4 +398,5 @@ export const {
354398
addColumn,
355399
deleteRow,
356400
deleteColumn,
401+
mergeCells,
357402
} = actions;

src/lib/svg/icons.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ type AvailableIcons =
33
| "align-right"
44
| "align-center"
55
| "align-justify"
6-
| "right-arrow";
6+
| "right-arrow"
7+
| "merge";
78
const Icons = ({ type }: { type: AvailableIcons }) => {
89
switch (type) {
910
case "align-left":
@@ -96,6 +97,22 @@ const Icons = ({ type }: { type: AvailableIcons }) => {
9697
<path d="M246.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 256c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l128-128z" />
9798
</svg>
9899
);
100+
case "merge":
101+
return (
102+
<svg
103+
width="16px"
104+
height="16px"
105+
viewBox="0 0 24 24"
106+
fill="none"
107+
xmlns="http://www.w3.org/2000/svg"
108+
>
109+
<path d="M12.5 21H17.75C19.5449 21 21 19.5449 21 17.75V17H12.5V21Z" fill="#212121" />
110+
<path d="M21 7V6.25C21 4.45507 19.5449 3 17.75 3H12.5V7H21Z" fill="#212121" />
111+
<path d="M11 3H6.25C4.45507 3 3 4.45507 3 6.25V7H11V3Z" fill="#212121" />
112+
<path d="M3 8.5V15.5H21V8.5H3Z" fill="#212121" />
113+
<path d="M3 17V17.75C3 19.5449 4.45507 21 6.25 21H11V17H3Z" fill="#212121" />
114+
</svg>
115+
);
99116
}
100117
};
101118
export default Icons;

todo.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ A row filter option from row column headers.
1313
Custom options in context menu.
1414

1515
Real-time Updates (Multiple users can edit same time): how de we enable realtime in this one?
16-
Merge cells
1716
Find end of rows having actual values, onSave and export only use rows till end of actual values
1817
Support adding images
1918
Locale - translations.
@@ -22,4 +21,6 @@ More events - on select, on bulk change, onUndo, onRedo, onScroll(which should h
2221
Custom tools - onClick should respond with all selected cell data
2322
Resize rows
2423
Multiple selection with cntrl key
25-
Readonly should disable all updates
24+
Readonly should disable all updates and support merged cells
25+
26+
!!complete pending tests

0 commit comments

Comments
 (0)