@hzab/edit-table 0.1.13-alpha.0 → 0.1.13
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/CHANGELOG.md +19 -0
- package/dist/common/WidthWrapHoc.d.ts +2 -0
- package/dist/common/useDynamicHeight.d.ts +8 -0
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +352 -199
- package/dist/index.js.map +1 -1
- package/dist/project-manifest.json +6 -5
- package/dist/project.json +4 -3
- package/package.json +5 -5
- package/src/Template/GetBaseTemplate.tsx +1 -1
- package/src/common/WidthWrapHoc.tsx +37 -8
- package/src/common/useDynamicHeight.ts +239 -0
- package/src/index.less +33 -1
- package/src/index.tsx +162 -129
package/src/index.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { ExpandListTemplate } from "./Template/ExpandListTemplate";
|
|
|
17
17
|
import { convertColumnsToRows, flattenColumns, isReactElement } from "./common/utils";
|
|
18
18
|
import { childAntdComponent } from "./common/childAntdComponent";
|
|
19
19
|
import { GetBaseTemplate } from "./Template/GetBaseTemplate";
|
|
20
|
+
import { useDynamicHeight } from "./common/useDynamicHeight";
|
|
20
21
|
import "./index.less";
|
|
21
22
|
import "@silevis/reactgrid/styles.css";
|
|
22
23
|
|
|
@@ -38,9 +39,41 @@ interface CellEditTableProps {
|
|
|
38
39
|
loading?: boolean;
|
|
39
40
|
isExpand?: boolean;
|
|
40
41
|
getList?: () => void;
|
|
42
|
+
setList?: (value: any) => void;
|
|
41
43
|
onRow?: (v: any, ri: number, ci: number) => void;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** 将编辑后的行合并回列表数据源,供 getList 返回前保持单元格新值 */
|
|
47
|
+
function mergeRowsByKey(list: any[], updates: any[], key: string): any[] {
|
|
48
|
+
if (!Array.isArray(list) || !Array.isArray(updates) || updates.length === 0) return list;
|
|
49
|
+
|
|
50
|
+
const updateMap = new Map<string, any>();
|
|
51
|
+
updates.forEach((item) => {
|
|
52
|
+
if (item?.[key] != null) updateMap.set(String(item[key]), item);
|
|
53
|
+
if (item?.id != null) updateMap.set(String(item.id), item);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const walk = (items: any[]): any[] => {
|
|
57
|
+
if (!Array.isArray(items)) return items;
|
|
58
|
+
return items.map((item) => {
|
|
59
|
+
const update = updateMap.get(String(item?.[key] ?? item?.id));
|
|
60
|
+
const originalChildren = item?.children;
|
|
61
|
+
const children = originalChildren ? walk(originalChildren) : originalChildren;
|
|
62
|
+
if (!update) {
|
|
63
|
+
return children === originalChildren ? item : { ...item, children };
|
|
64
|
+
}
|
|
65
|
+
const { children: _children, ...nextItem } = update;
|
|
66
|
+
return {
|
|
67
|
+
...item,
|
|
68
|
+
...nextItem,
|
|
69
|
+
...(originalChildren !== undefined ? { children } : {}),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return walk(list);
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
45
78
|
const {
|
|
46
79
|
columns,
|
|
@@ -49,8 +82,8 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
49
82
|
onChange,
|
|
50
83
|
reactGridProps = {},
|
|
51
84
|
onEditSubmit,
|
|
52
|
-
headerRowHeight =
|
|
53
|
-
rowHeight =
|
|
85
|
+
headerRowHeight = 48,
|
|
86
|
+
rowHeight = 48,
|
|
54
87
|
reactGridStyle,
|
|
55
88
|
className,
|
|
56
89
|
Slots,
|
|
@@ -60,6 +93,7 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
60
93
|
loading,
|
|
61
94
|
isExpand = false,
|
|
62
95
|
getList,
|
|
96
|
+
setList,
|
|
63
97
|
onRow,
|
|
64
98
|
} = props;
|
|
65
99
|
|
|
@@ -74,82 +108,26 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
74
108
|
const [headerCustomTemplate, setHeaderCustomTemplate] = useState<any>([]);
|
|
75
109
|
const [highlights, setHighlights] = useState([]);
|
|
76
110
|
const highlightsRowIdRef = useRef();
|
|
77
|
-
/** 附件等动态行高:rowId -> { [columnId]: height },同行多附件列时取最大值 */
|
|
78
|
-
const [rowHeights, setRowHeights] = useState<any>({});
|
|
79
|
-
/** 高度上报按帧合并缓冲,一帧内多次上报只触发一次 state 更新 */
|
|
80
|
-
const pendingHeightsRef = useRef<any>({});
|
|
81
|
-
const flushHeightsRafRef = useRef<any>();
|
|
82
111
|
const gridRef = useRef<any>();
|
|
112
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
83
113
|
const currentCell = useRef<any>();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (rowChanged) {
|
|
102
|
-
if (next === prev) next = { ...prev };
|
|
103
|
-
next[rowId] = cols;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return next === prev ? prev : next;
|
|
107
|
-
});
|
|
108
|
-
}, []);
|
|
109
|
-
|
|
110
|
-
/** 供单元格(如 Upload 附件)回传内容高度,撑开/恢复所在行高 */
|
|
111
|
-
const handleRowHeightChange = useCallback(
|
|
112
|
-
(rowId: any, height?: number, columnId?: string) => {
|
|
113
|
-
const h = Math.max(Number(height) || 0, rowHeight);
|
|
114
|
-
const rId = String(rowId);
|
|
115
|
-
const cId = columnId || "_";
|
|
116
|
-
const row = (pendingHeightsRef.current[rId] = pendingHeightsRef.current[rId] || {});
|
|
117
|
-
if (row[cId] === h) return;
|
|
118
|
-
row[cId] = h;
|
|
119
|
-
if (!flushHeightsRafRef.current) {
|
|
120
|
-
flushHeightsRafRef.current = requestAnimationFrame(flushPendingHeights);
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
[rowHeight, flushPendingHeights],
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
/** 组件卸载时取消未执行的合帧刷新 */
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
return () => {
|
|
129
|
-
if (flushHeightsRafRef.current) cancelAnimationFrame(flushHeightsRafRef.current);
|
|
130
|
-
};
|
|
131
|
-
}, []);
|
|
132
|
-
|
|
133
|
-
/** 行高变化时仅补丁式更新受影响行的 height,避免整表重建 */
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
setRows((prev: any) => {
|
|
136
|
-
if (!Array.isArray(prev) || prev.length === 0) return prev;
|
|
137
|
-
let changed = false;
|
|
138
|
-
const next = prev.map((row) => {
|
|
139
|
-
// 表头行高度固定,不参与动态行高
|
|
140
|
-
if (typeof row?.rowId === "string" && row.rowId.indexOf("header-") === 0) return row;
|
|
141
|
-
const heightMap = rowHeights[row.rowId];
|
|
142
|
-
const h =
|
|
143
|
-
heightMap && Object.keys(heightMap).length > 0
|
|
144
|
-
? Math.max(rowHeight, ...Object.values(heightMap).map(Number))
|
|
145
|
-
: rowHeight;
|
|
146
|
-
if (row.height === h) return row;
|
|
147
|
-
changed = true;
|
|
148
|
-
return { ...row, height: h };
|
|
149
|
-
});
|
|
150
|
-
return changed ? next : prev;
|
|
151
|
-
});
|
|
152
|
-
}, [rowHeights, rowHeight]);
|
|
114
|
+
/** 展开态镜像 ref:dataSource 同步 effect 读取最新 expandKeys 用(不加进 deps,
|
|
115
|
+
* 避免展开/收起时又触发全量同步——展开/收起由 ExpandListTemplate 直接维护 newDataSource) */
|
|
116
|
+
const expandKeysRef = useRef<any>([]);
|
|
117
|
+
expandKeysRef.current = expandKeys;
|
|
118
|
+
/** 动态测量单元格高度及滚动逻辑 */
|
|
119
|
+
const {
|
|
120
|
+
handleRowHeightChange,
|
|
121
|
+
armScrollProtect,
|
|
122
|
+
restoreScroll,
|
|
123
|
+
SCROLL_RESET_GUARD_MS,
|
|
124
|
+
rowsRef,
|
|
125
|
+
rowHeights,
|
|
126
|
+
} = useDynamicHeight({
|
|
127
|
+
wrapRef,
|
|
128
|
+
rowHeight,
|
|
129
|
+
setRows,
|
|
130
|
+
});
|
|
153
131
|
|
|
154
132
|
useImperativeHandle(ref, () => {
|
|
155
133
|
return {
|
|
@@ -165,10 +143,28 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
165
143
|
}
|
|
166
144
|
});
|
|
167
145
|
};
|
|
146
|
+
|
|
168
147
|
/** 洗数据 */
|
|
169
148
|
useEffect(() => {
|
|
170
149
|
handleDataSource(dataSource);
|
|
171
|
-
|
|
150
|
+
// 同步时必须保留展开态:把已展开父行的 children 重新插入到对应位置。
|
|
151
|
+
// 直接 setNewDataSource(dataSource) 会把列表重置为仅父行,子行瞬间从矩阵消失,
|
|
152
|
+
// ReactGrid 内部 validateLocation(focusedLocation) 在 rowId 查不到时会被
|
|
153
|
+
// idx-clamp 永久替换成原索引处的其他行,导致聚焦框/整行高亮错位。
|
|
154
|
+
const flattenWithExpansion = (arr: any) => {
|
|
155
|
+
const result: any = [];
|
|
156
|
+
const loop = (list: any) => {
|
|
157
|
+
for (const item of list || []) {
|
|
158
|
+
result.push(item);
|
|
159
|
+
if (item?.children?.length && expandKeysRef.current?.includes(item?.[rowKey])) {
|
|
160
|
+
loop(item.children);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
loop(arr);
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
setNewDataSource(flattenWithExpansion(dataSource));
|
|
172
168
|
}, [dataSource]);
|
|
173
169
|
|
|
174
170
|
/** 判断当前选中行是否还存在 */
|
|
@@ -176,6 +172,8 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
176
172
|
const validRowIds = rows.map((r) => r.rowId);
|
|
177
173
|
if (!validRowIds?.includes(highlightsRowIdRef.current)) {
|
|
178
174
|
setHighlights([]);
|
|
175
|
+
// 同步清理高亮行全局标记,避免 .rg-cell 残留高亮 class
|
|
176
|
+
(window as any).gridHighlightRowId = null;
|
|
179
177
|
}
|
|
180
178
|
}, [rows]);
|
|
181
179
|
|
|
@@ -320,68 +318,95 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
320
318
|
};
|
|
321
319
|
});
|
|
322
320
|
const mergerList = [...headerList, ...tempDataSource];
|
|
321
|
+
// 整表重建会触发 ReactGrid 内部程序化写 scrollTop(滚动回顶部):
|
|
322
|
+
// 重建前记录用户滚动位置并开启保护窗口,重建完成后双层 rAF 恢复;
|
|
323
|
+
// 窗口内若再发生归零式重置,由 scroll 监听纠正回记忆位置
|
|
324
|
+
rowsRef.current = mergerList;
|
|
325
|
+
armScrollProtect();
|
|
323
326
|
setRows(mergerList);
|
|
327
|
+
restoreScroll();
|
|
324
328
|
// 注:此处读取的 rowHeights 为本次渲染时的快照;行高的后续增量变化由下方补丁 effect 处理,避免全量重建
|
|
325
329
|
}, [newDataSource, tableColumns, headerList]);
|
|
326
|
-
|
|
327
|
-
if (loading) {
|
|
328
|
-
const item = rows?.find((el) => el?.rowId == currentCell.current?.rowId);
|
|
329
|
-
let newItem = item?.cells?.find((it) => it?.columnId == currentCell.current?.columnId);
|
|
330
|
-
newItem = { ...currentCell.current };
|
|
331
|
-
initRowsRef.current = rows;
|
|
332
|
-
}
|
|
333
|
-
}, [loading, rows]);
|
|
330
|
+
|
|
334
331
|
// 单元格编辑回调
|
|
335
332
|
const handleCellsChanged = async (changes) => {
|
|
336
|
-
const batchQuery: any = [];
|
|
337
333
|
if (!Array.isArray(changes) || changes.length === 0) return;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
isBatchSubmit = true;
|
|
342
|
-
}
|
|
334
|
+
const batchQuery: any = [];
|
|
335
|
+
// 父级数据源快照:保存失败时撤销乐观更新,避免 getList 前再次回显旧值
|
|
336
|
+
const previousDataSource = dataSource;
|
|
343
337
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
value: item?.setCellData && item?.setCellData(prevRows[changeRowIdx]),
|
|
359
|
-
style:
|
|
360
|
-
item?.setCellStyle &&
|
|
361
|
-
item?.setCellStyle(
|
|
362
|
-
(item?.setCellData && item?.setCellData(prevRows[changeRowIdx])) ||
|
|
363
|
-
prevRows[changeRowIdx],
|
|
364
|
-
),
|
|
365
|
-
};
|
|
366
|
-
prevRows[changeRowIdx].cells[resultColumnIdx] = newCell;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (onEditSubmit && !isBatchSubmit) {
|
|
370
|
-
try {
|
|
371
|
-
await onEditSubmit(batchQuery);
|
|
372
|
-
} catch (err) {
|
|
373
|
-
getList && getList();
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
onChange && onChange(prevRows[changeRowIdx]);
|
|
338
|
+
// 基于 rowsRef 当前值在 updater 外纯计算(updater 执行时机不确定且可能执行两次,
|
|
339
|
+
// 不能在 updater 内收集提交数据),一次性生成新数组后同步 rowsRef 再 setRows
|
|
340
|
+
const prevRows = rowsRef.current || [];
|
|
341
|
+
const nextRows = prevRows.map((row) => ({ ...row, cells: [...row.cells] }));
|
|
342
|
+
// 编辑前单元格快照,保存失败时用于单元格级回滚
|
|
343
|
+
const prevCellSnapshot: any[] = [];
|
|
344
|
+
changes.forEach((change) => {
|
|
345
|
+
const changeRowIdx = nextRows.findIndex((el) => el.rowId == change.rowId);
|
|
346
|
+
const changeColumnIdx = tableColumns.findIndex((el) => el.columnId == change.columnId);
|
|
347
|
+
if (changeRowIdx === -1 || changeColumnIdx === -1) return;
|
|
348
|
+
prevCellSnapshot.push({
|
|
349
|
+
rowId: change.rowId,
|
|
350
|
+
columnId: change.columnId,
|
|
351
|
+
previousCell: change.previousCell,
|
|
377
352
|
});
|
|
378
|
-
|
|
353
|
+
nextRows[changeRowIdx].cells[changeColumnIdx] = { ...change.newCell };
|
|
354
|
+
currentCell.current = { ...change.newCell };
|
|
355
|
+
batchQuery.push(handleArrayToObj(nextRows[changeRowIdx]));
|
|
356
|
+
if (formulaConfig) {
|
|
357
|
+
for (const key in formulaConfig) {
|
|
358
|
+
const item = formulaConfig[key];
|
|
359
|
+
const resultColumnIdx = tableColumns.findIndex((el) => el.columnId == key);
|
|
360
|
+
if (resultColumnIdx === -1) continue;
|
|
361
|
+
const newCell = {
|
|
362
|
+
...nextRows[changeRowIdx].cells[resultColumnIdx],
|
|
363
|
+
text: item?.setCellData && item?.setCellData(nextRows[changeRowIdx]),
|
|
364
|
+
value: item?.setCellData && item?.setCellData(nextRows[changeRowIdx]),
|
|
365
|
+
style:
|
|
366
|
+
item?.setCellStyle &&
|
|
367
|
+
item?.setCellStyle(
|
|
368
|
+
(item?.setCellData && item?.setCellData(nextRows[changeRowIdx])) ||
|
|
369
|
+
nextRows[changeRowIdx],
|
|
370
|
+
),
|
|
371
|
+
};
|
|
372
|
+
nextRows[changeRowIdx].cells[resultColumnIdx] = newCell;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
onChange && onChange(nextRows[changeRowIdx]);
|
|
379
376
|
});
|
|
377
|
+
rowsRef.current = nextRows;
|
|
378
|
+
// 编辑提交后 list-render 的 onEditSubmit 会全量 getList 刷新并触发 rows 重建,
|
|
379
|
+
// 期间任意时刻都可能发生程序化回顶部:开启长保护窗,覆盖 提交→refetch→重建 全链路
|
|
380
|
+
armScrollProtect(SCROLL_RESET_GUARD_MS);
|
|
381
|
+
setRows(nextRows);
|
|
382
|
+
restoreScroll();
|
|
383
|
+
// 同步父级 list:保存成功后的 getList 在接口返回前仍使用旧 list,
|
|
384
|
+
// 只更新内部 rows 会被随后触发的数据源重建覆盖成编辑前的值。
|
|
385
|
+
if (setList && batchQuery.length > 0) {
|
|
386
|
+
setList((prevList: any[]) => mergeRowsByKey(prevList, batchQuery, rowKey));
|
|
387
|
+
}
|
|
380
388
|
|
|
381
|
-
|
|
389
|
+
// 提交成功后由 list-render 的 onEditSubmit 全量刷新列表;
|
|
390
|
+
// 滚动位置由 rows 重建 effect 中的快照/恢复机制保持;
|
|
391
|
+
// 失败时先把修改过的单元格回滚为编辑前的值,再 getList() 兜底刷新
|
|
392
|
+
if (onEditSubmit) {
|
|
382
393
|
try {
|
|
383
|
-
|
|
394
|
+
await onEditSubmit(batchQuery);
|
|
384
395
|
} catch (err) {
|
|
396
|
+
const restoredRows = (rowsRef.current || []).map((row) => ({
|
|
397
|
+
...row,
|
|
398
|
+
cells: [...row.cells],
|
|
399
|
+
}));
|
|
400
|
+
prevCellSnapshot.forEach(({ rowId, columnId, previousCell }) => {
|
|
401
|
+
const rIdx = restoredRows.findIndex((el) => el.rowId == rowId);
|
|
402
|
+
const cIdx = tableColumns.findIndex((el) => el.columnId == columnId);
|
|
403
|
+
if (rIdx === -1 || cIdx === -1 || !previousCell) return;
|
|
404
|
+
restoredRows[rIdx].cells[cIdx] = { ...previousCell };
|
|
405
|
+
});
|
|
406
|
+
rowsRef.current = restoredRows;
|
|
407
|
+
setRows(restoredRows);
|
|
408
|
+
restoreScroll();
|
|
409
|
+
setList?.(previousDataSource);
|
|
385
410
|
getList && getList();
|
|
386
411
|
}
|
|
387
412
|
}
|
|
@@ -518,15 +543,23 @@ function CellEditTable(props: CellEditTableProps, ref: any) {
|
|
|
518
543
|
borderColor: "transparent",
|
|
519
544
|
}));
|
|
520
545
|
highlightsRowIdRef.current = cell?.rowId;
|
|
546
|
+
// 记录高亮行 rowId 到全局,供 WidthWrapHoc.getClassName 给 .rg-cell 追加高亮 class
|
|
547
|
+
(window as any).gridHighlightRowId = cell?.rowId;
|
|
548
|
+
|
|
521
549
|
setHighlights(list);
|
|
522
550
|
};
|
|
523
551
|
|
|
524
552
|
return (
|
|
525
553
|
<Spin spinning={loading}>
|
|
526
|
-
<div className={`grid-container ${className}`} style={reactGridStyle}>
|
|
554
|
+
<div ref={wrapRef} className={`grid-container ${className}`} style={reactGridStyle}>
|
|
527
555
|
<ReactGrid
|
|
528
556
|
ref={gridRef}
|
|
529
|
-
rows
|
|
557
|
+
// 直接渲染 rows state:loading 期间它本就持有完整数据(编辑提交时已 setRows)。
|
|
558
|
+
// 历史上曾用 initRowsRef 快照在 loading 期间代替 rows,但该 ref 由 effect 写入、
|
|
559
|
+
// 晚于渲染消费——loading 翻 true 的当次渲染读到的是过期快照(行数少于当前),
|
|
560
|
+
// 矩阵瞬态缩小、展开子行消失,ReactGrid 内部 validateLocation 的 idx-clamp
|
|
561
|
+
// 会把 focusedLocation 永久替换成错误行(聚焦框/整行高亮错位)。
|
|
562
|
+
rows={rows}
|
|
530
563
|
key={dataSource?.length == 0 ? 0 : 1}
|
|
531
564
|
columns={tableColumns}
|
|
532
565
|
onCellsChanged={handleCellsChanged}
|