@linzjs/step-ag-grid 29.13.0 → 29.14.1
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.css +26 -2
- package/dist/src/components/Grid.d.ts +36 -61
- package/dist/src/components/GridRangeSelectContextMenu.d.ts +10 -0
- package/dist/src/components/gridForm/GridFormDropDown.d.ts +1 -1
- package/dist/src/components/gridForm/GridFormPopoverMenu.d.ts +2 -1
- package/dist/src/components/gridHook/useGridContextMenu.d.ts +12 -7
- package/dist/src/components/gridHook/useGridCopy.d.ts +15 -0
- package/dist/src/components/gridHook/useGridCopySettings.d.ts +11 -0
- package/dist/src/components/gridHook/useGridRangeSelection.d.ts +25 -0
- package/dist/src/components/gridPopoverEdit/GridPopoverEditBearing.d.ts +2 -0
- package/dist/src/components/types.d.ts +1 -0
- package/dist/src/contexts/GridContext.d.ts +1 -0
- package/dist/src/lui/timeoutHook.d.ts +0 -6
- package/dist/src/lui/tsUtils.d.ts +5 -0
- package/dist/src/utils/__tests__/random.ts +19 -0
- package/dist/src/utils/util.d.ts +1 -0
- package/dist/step-ag-grid.cjs +560 -79
- package/dist/step-ag-grid.cjs.map +1 -1
- package/dist/step-ag-grid.esm.js +561 -82
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Grid.tsx +249 -179
- package/src/components/GridRangeSelectContextMenu.tsx +73 -0
- package/src/components/gridForm/GridFormDropDown.tsx +1 -3
- package/src/components/gridForm/GridFormPopoverMenu.tsx +2 -1
- package/src/components/gridHook/useGridContextMenu.tsx +22 -9
- package/src/components/gridHook/useGridCopy.ts +279 -0
- package/src/components/gridHook/useGridCopySettings.ts +28 -0
- package/src/components/gridHook/useGridRangeSelection.ts +235 -0
- package/src/components/gridPopoverEdit/GridPopoverEditBearing.ts +3 -0
- package/src/components/types.ts +2 -0
- package/src/contexts/GridContext.tsx +2 -0
- package/src/contexts/GridContextProvider.tsx +5 -2
- package/src/lui/timeoutHook.tsx +0 -19
- package/src/lui/tsUtils.ts +8 -0
- package/src/stories/grid/GridCopy.stories.tsx +175 -0
- package/src/stories/grid/GridPopoverEditBearing.stories.tsx +4 -4
- package/src/stories/grid/GridReadOnly.stories.tsx +1 -0
- package/src/stories/grid/interactions/GridKeyboardInteractions.stories.tsx +2 -2
- package/src/styles/ContextMenu.scss +61 -0
- package/src/styles/Grid.scss +26 -2
- package/src/utils/__tests__/random.ts +19 -0
- package/src/utils/bearing.ts +2 -2
- package/src/utils/util.ts +2 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import '../../styles/ContextMenu.scss';
|
|
2
|
+
|
|
1
3
|
import { ColDef } from 'ag-grid-community';
|
|
2
4
|
import { CellContextMenuEvent } from 'ag-grid-community';
|
|
3
5
|
import { ReactElement, useCallback, useContext, useRef, useState } from 'react';
|
|
@@ -6,24 +8,30 @@ import { GridContext } from '../../contexts/GridContext';
|
|
|
6
8
|
import { ControlledMenu } from '../../react-menu3';
|
|
7
9
|
import { GridBaseRow } from '../types';
|
|
8
10
|
|
|
9
|
-
export interface GridContextMenuComponentProps<TData extends GridBaseRow> {
|
|
11
|
+
export interface GridContextMenuComponentProps<TData extends GridBaseRow, Context extends object = object> {
|
|
10
12
|
selectedRows: TData[];
|
|
11
13
|
clickedRow: TData;
|
|
12
14
|
colDef: ColDef;
|
|
13
15
|
close: () => void;
|
|
16
|
+
event?: CellContextMenuEvent;
|
|
17
|
+
context?: Context;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
export type GridContextMenuComponent<TData extends GridBaseRow> = (
|
|
17
|
-
props: GridContextMenuComponentProps<TData>,
|
|
20
|
+
export type GridContextMenuComponent<TData extends GridBaseRow, Context extends object = object> = (
|
|
21
|
+
props: GridContextMenuComponentProps<TData, Context>,
|
|
18
22
|
) => ReactElement | null;
|
|
19
23
|
|
|
20
|
-
export
|
|
24
|
+
export interface useGridContextMenuProps<TData extends GridBaseRow, Context extends object = object> {
|
|
25
|
+
contextMenuSelectRow?: boolean;
|
|
26
|
+
contextMenu?: GridContextMenuComponent<TData, Context>;
|
|
27
|
+
context?: Context;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const useGridContextMenu = <TData extends GridBaseRow, Context extends object = object>({
|
|
21
31
|
contextMenuSelectRow,
|
|
22
32
|
contextMenu: ContextMenu,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
contextMenu?: GridContextMenuComponent<TData>;
|
|
26
|
-
}) => {
|
|
33
|
+
context,
|
|
34
|
+
}: useGridContextMenuProps<TData, Context>) => {
|
|
27
35
|
const { redrawRows, prePopupOps, resetFocusedCellAfterCellEditing, getSelectedRows } = useContext(GridContext);
|
|
28
36
|
|
|
29
37
|
const [isOpen, setOpen] = useState(false);
|
|
@@ -33,6 +41,7 @@ export const useGridContextMenu = <TData extends GridBaseRow>({
|
|
|
33
41
|
const clickedColDefRef = useRef<ColDef>(null!);
|
|
34
42
|
const selectedRowsRef = useRef<any[]>([]);
|
|
35
43
|
const clickedRowRef = useRef<any>(null);
|
|
44
|
+
const eventRef = useRef<CellContextMenuEvent | undefined>(undefined);
|
|
36
45
|
|
|
37
46
|
const openMenu = useCallback(
|
|
38
47
|
(e: PointerEvent | null | undefined) => {
|
|
@@ -59,6 +68,7 @@ export const useGridContextMenu = <TData extends GridBaseRow>({
|
|
|
59
68
|
clickedColDefRef.current = event.colDef;
|
|
60
69
|
selectedRowsRef.current = getSelectedRows();
|
|
61
70
|
clickedRowRef.current = event.data;
|
|
71
|
+
eventRef.current = event;
|
|
62
72
|
|
|
63
73
|
// This is actually a pointer event
|
|
64
74
|
openMenu(event.event as PointerEvent);
|
|
@@ -69,9 +79,10 @@ export const useGridContextMenu = <TData extends GridBaseRow>({
|
|
|
69
79
|
return {
|
|
70
80
|
openMenu,
|
|
71
81
|
cellContextMenu,
|
|
72
|
-
|
|
82
|
+
contextMenuComponent: ContextMenu ? (
|
|
73
83
|
<>
|
|
74
84
|
<ControlledMenu
|
|
85
|
+
className={'GridContextMenu'}
|
|
75
86
|
anchorPoint={anchorPoint}
|
|
76
87
|
state={isOpen ? 'open' : 'closed'}
|
|
77
88
|
direction="right"
|
|
@@ -82,7 +93,9 @@ export const useGridContextMenu = <TData extends GridBaseRow>({
|
|
|
82
93
|
selectedRows={selectedRowsRef.current}
|
|
83
94
|
clickedRow={clickedRowRef.current}
|
|
84
95
|
colDef={clickedColDefRef.current}
|
|
96
|
+
event={eventRef.current}
|
|
85
97
|
close={closeMenu}
|
|
98
|
+
context={context}
|
|
86
99
|
/>
|
|
87
100
|
)}
|
|
88
101
|
</ControlledMenu>
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { useShowLUIMessage } from '@linzjs/lui';
|
|
2
|
+
import { CellContextMenuEvent, IRowNode } from 'ag-grid-community';
|
|
3
|
+
import { compact } from 'lodash-es';
|
|
4
|
+
import { ClipboardEvent as ReactClipboardEvent, MutableRefObject, useCallback } from 'react';
|
|
5
|
+
|
|
6
|
+
import { useGridContext } from '../../contexts/GridContext';
|
|
7
|
+
import { CopyOptionsContext, GridRangeSelectContextMenu } from '../GridRangeSelectContextMenu';
|
|
8
|
+
import { agGridSelectRowColId, GridBaseRow } from '../types';
|
|
9
|
+
import { useGridContextMenu } from './useGridContextMenu';
|
|
10
|
+
import { CopyOptionsKey, gridCopyOptions, useGridCopySettings } from './useGridCopySettings';
|
|
11
|
+
import { CellLocation, GridRanges } from './useGridRangeSelection';
|
|
12
|
+
|
|
13
|
+
export const useGridCopy = <TData extends GridBaseRow>({
|
|
14
|
+
ranges,
|
|
15
|
+
rangeStartRef,
|
|
16
|
+
rangeEndRef,
|
|
17
|
+
hasSelectedMoreThanOneCellRef,
|
|
18
|
+
cellContextMenu,
|
|
19
|
+
}: {
|
|
20
|
+
ranges: () => GridRanges<TData>;
|
|
21
|
+
rangeStartRef: MutableRefObject<CellLocation | null>;
|
|
22
|
+
rangeEndRef: MutableRefObject<CellLocation | null>;
|
|
23
|
+
hasSelectedMoreThanOneCellRef: MutableRefObject<boolean>;
|
|
24
|
+
cellContextMenu: (event: CellContextMenuEvent) => void;
|
|
25
|
+
}) => {
|
|
26
|
+
const showToast = useShowLUIMessage();
|
|
27
|
+
const { getSelectedRowIds } = useGridContext<TData>();
|
|
28
|
+
const { getColDef, getCellValue } = useGridContext<TData>();
|
|
29
|
+
const { copyType, setCopyType } = useGridCopySettings();
|
|
30
|
+
|
|
31
|
+
const onCopy = useCallback(
|
|
32
|
+
(type: CopyOptionsKey = copyType) => {
|
|
33
|
+
const rangeStart = rangeStartRef.current;
|
|
34
|
+
const rangeEnd = rangeEndRef.current;
|
|
35
|
+
if (rangeStart === null || rangeEnd === null || !hasSelectedMoreThanOneCellRef.current) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (rangeStart.rowId === rangeEnd.rowId && rangeStart.colId === rangeEnd.colId) {
|
|
40
|
+
type = 'html';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const json = type === 'json';
|
|
44
|
+
const { selectedColIds, selectedNodes } = ranges();
|
|
45
|
+
const filteredSelectedColIds = selectedColIds.filter(
|
|
46
|
+
(colId) =>
|
|
47
|
+
colId === agGridSelectRowColId ||
|
|
48
|
+
(colId !== 'gridCellFiller' && getColDef(colId)?.headerComponentParams?.exportable !== false),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const selectedRowIds = getSelectedRowIds();
|
|
52
|
+
const formatters = compact(
|
|
53
|
+
filteredSelectedColIds.map((colKey) => {
|
|
54
|
+
return (rowNode: IRowNode): string | number | boolean | null | undefined => {
|
|
55
|
+
if (colKey === agGridSelectRowColId) {
|
|
56
|
+
return selectedRowIds.includes(rowNode.data.id);
|
|
57
|
+
} else {
|
|
58
|
+
const v = getCellValue({ rowNode, colKey });
|
|
59
|
+
if (json && v !== undefined) {
|
|
60
|
+
return v;
|
|
61
|
+
}
|
|
62
|
+
const f = getCellValue({ rowNode, colKey, useFormatter: true });
|
|
63
|
+
// If it's a number, and it matches value return the original type
|
|
64
|
+
return v == f ? v : f;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Get and apply headers
|
|
71
|
+
const headers = filteredSelectedColIds.map((colId) => {
|
|
72
|
+
if (colId === agGridSelectRowColId) return type === 'json' ? 'selected' : 'Selected';
|
|
73
|
+
if (json) {
|
|
74
|
+
return colId;
|
|
75
|
+
}
|
|
76
|
+
return getColDef(colId)?.headerName ?? '?';
|
|
77
|
+
});
|
|
78
|
+
const maxCellLength: Record<string, number> = {};
|
|
79
|
+
headers.forEach((headerName, i) => {
|
|
80
|
+
const colId = filteredSelectedColIds[i];
|
|
81
|
+
maxCellLength[colId] = headerName.length;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const rows: string[][] = [];
|
|
85
|
+
if (type === 'csv' || type === 'html') {
|
|
86
|
+
rows.push(headers);
|
|
87
|
+
}
|
|
88
|
+
selectedNodes.forEach((node) => {
|
|
89
|
+
const row: string[] = [];
|
|
90
|
+
rows.push(row);
|
|
91
|
+
formatters.forEach((formatter, i) => {
|
|
92
|
+
const colId = filteredSelectedColIds[i];
|
|
93
|
+
let value = formatter(node);
|
|
94
|
+
if (typeof value === 'string') {
|
|
95
|
+
value = value.replaceAll('\xa0', ' ');
|
|
96
|
+
}
|
|
97
|
+
if (!json) {
|
|
98
|
+
if (value === '-' || value === '–' || value == null) {
|
|
99
|
+
value = '';
|
|
100
|
+
}
|
|
101
|
+
value = String(value);
|
|
102
|
+
if (value === '-' || value === '–') {
|
|
103
|
+
value = '';
|
|
104
|
+
}
|
|
105
|
+
maxCellLength[colId] = Math.max(maxCellLength[colId], value.length);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
switch (type) {
|
|
109
|
+
case 'json':
|
|
110
|
+
value =
|
|
111
|
+
value === 'undefined' || (value !== null && typeof value === 'object') || typeof value === 'string'
|
|
112
|
+
? JSON.stringify(value)
|
|
113
|
+
: value;
|
|
114
|
+
break;
|
|
115
|
+
case 'markdown':
|
|
116
|
+
value = encodeMarkdownValue(value as string);
|
|
117
|
+
break;
|
|
118
|
+
case 'csv':
|
|
119
|
+
value = encodeCSVValue(value as string);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
row.push(String(value));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
let result = '';
|
|
127
|
+
let html = `<table style="
|
|
128
|
+
font-family: 'Open Sans', system-ui, sans-serif;
|
|
129
|
+
font-size: 10px;
|
|
130
|
+
line-height: 10px;
|
|
131
|
+
border: 1px solid #d1d9e0;
|
|
132
|
+
border-collapse: collapse;
|
|
133
|
+
">`;
|
|
134
|
+
if (json) {
|
|
135
|
+
result += '[\n';
|
|
136
|
+
}
|
|
137
|
+
rows.forEach((row, rowIndex) => {
|
|
138
|
+
if (json) {
|
|
139
|
+
result += ' { ';
|
|
140
|
+
}
|
|
141
|
+
if (rowIndex == 1) {
|
|
142
|
+
html += '<tbody style="font-weight: 400;">';
|
|
143
|
+
}
|
|
144
|
+
if (rowIndex == 0) {
|
|
145
|
+
html += '<thead style="font-weight: 600;">';
|
|
146
|
+
}
|
|
147
|
+
if (rowIndex === 0 || (rowIndex & 1) === 1) {
|
|
148
|
+
html += '<tr>';
|
|
149
|
+
} else {
|
|
150
|
+
html += '<tr style="background-color: #f6f8fa;">';
|
|
151
|
+
}
|
|
152
|
+
if (rowIndex === 1 && type === 'html') {
|
|
153
|
+
Object.values(maxCellLength).forEach((maxLength) => {
|
|
154
|
+
result += '|' + '-'.repeat(maxLength + 2);
|
|
155
|
+
});
|
|
156
|
+
result += '|\n';
|
|
157
|
+
}
|
|
158
|
+
row.forEach((cell, i) => {
|
|
159
|
+
switch (type) {
|
|
160
|
+
case 'csv':
|
|
161
|
+
if (i !== 0) {
|
|
162
|
+
result += ', ';
|
|
163
|
+
}
|
|
164
|
+
result += cell;
|
|
165
|
+
break;
|
|
166
|
+
case 'html':
|
|
167
|
+
if (i === 0) {
|
|
168
|
+
result += '|';
|
|
169
|
+
}
|
|
170
|
+
const colId = filteredSelectedColIds[i];
|
|
171
|
+
result += ' ' + cell.padEnd(maxCellLength[colId], ' ') + ' ';
|
|
172
|
+
result += '|';
|
|
173
|
+
html +=
|
|
174
|
+
rowIndex === 0
|
|
175
|
+
? '<th style="border: 1px solid #d1d9e0; padding:6px 13px; text-align: left;">'
|
|
176
|
+
: '<td style="border: 1px solid #d1d9e0; padding:6px 13px;">';
|
|
177
|
+
html += cell;
|
|
178
|
+
html += rowIndex === 0 ? '</th>' : '</td>';
|
|
179
|
+
break;
|
|
180
|
+
case 'json':
|
|
181
|
+
if (i !== 0) {
|
|
182
|
+
result += ', ';
|
|
183
|
+
}
|
|
184
|
+
result += JSON.stringify(headers[i]) + ': ' + cell;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
html += '</tr>';
|
|
189
|
+
if (rowIndex == 0) {
|
|
190
|
+
html += '</thead>';
|
|
191
|
+
}
|
|
192
|
+
if (rowIndex == rows.length - 1) {
|
|
193
|
+
html += '</tbody>';
|
|
194
|
+
}
|
|
195
|
+
if (json) {
|
|
196
|
+
result += ' }';
|
|
197
|
+
}
|
|
198
|
+
result += '\n';
|
|
199
|
+
});
|
|
200
|
+
if (json) {
|
|
201
|
+
result += ']\n';
|
|
202
|
+
}
|
|
203
|
+
html += '</table>';
|
|
204
|
+
|
|
205
|
+
showToast({
|
|
206
|
+
message: `${gridCopyOptions[type].text} copied`,
|
|
207
|
+
messageType: 'toast',
|
|
208
|
+
messageLevel: 'info',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (type === 'html') {
|
|
212
|
+
const clipboardItem = new ClipboardItem({
|
|
213
|
+
'text/html': new Blob([html], { type: 'text/html' }),
|
|
214
|
+
'text/plain': new Blob([result], { type: 'text/plain' }), // fallback
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
void navigator.clipboard.write([clipboardItem]).catch((err) => console.error('Failed to copy: ', err));
|
|
218
|
+
} else {
|
|
219
|
+
void navigator.clipboard.writeText(result).catch((err) => console.error('Failed to copy: ', err));
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
[getCellValue, ranges, copyType],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const onCopyEvent = useCallback(
|
|
226
|
+
(e: ReactClipboardEvent<HTMLDivElement>) => {
|
|
227
|
+
const rangeStart = rangeStartRef.current;
|
|
228
|
+
const rangeEnd = rangeEndRef.current;
|
|
229
|
+
if (
|
|
230
|
+
rangeStart === null ||
|
|
231
|
+
rangeEnd === null ||
|
|
232
|
+
(rangeStart.colId === rangeEnd.colId &&
|
|
233
|
+
rangeStart.rowId === rangeEnd.rowId &&
|
|
234
|
+
!hasSelectedMoreThanOneCellRef.current)
|
|
235
|
+
) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
|
|
241
|
+
onCopy();
|
|
242
|
+
},
|
|
243
|
+
[onCopy],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const { cellContextMenu: rangeSelectContextMenu, contextMenuComponent: rangeSelectContextMenuComponent } =
|
|
247
|
+
useGridContextMenu<TData, CopyOptionsContext>({
|
|
248
|
+
contextMenu: GridRangeSelectContextMenu<TData>,
|
|
249
|
+
context: { onCopy, copyType, setCopyType },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const rangeSelectInterceptContextMenu = useCallback(
|
|
253
|
+
(event: CellContextMenuEvent<TData>) =>
|
|
254
|
+
rangeStartRef.current == null ? cellContextMenu(event) : rangeSelectContextMenu(event),
|
|
255
|
+
[cellContextMenu, rangeSelectContextMenu],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
onCopyEvent,
|
|
260
|
+
rangeSelectInterceptContextMenu,
|
|
261
|
+
rangeSelectContextMenuComponent,
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const encodeMarkdownValue = (value: string): string => {
|
|
266
|
+
return value.replaceAll('|', '\\|');
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const encodeCSVValue = (value: string): string => {
|
|
270
|
+
let str = String(value);
|
|
271
|
+
|
|
272
|
+
// Check if it needs quoting
|
|
273
|
+
if (/[",\n]/.test(str)) {
|
|
274
|
+
// Escape double quotes by doubling them
|
|
275
|
+
str = '"' + str.replace(/"/g, '""') + '"';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return str;
|
|
279
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { IconName } from '@linzjs/lui/dist/components/LuiIcon/LuiIcon';
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export const gridCopyOptions: Record<string, { text: string; icon: IconName; developer?: boolean }> = {
|
|
5
|
+
html: { text: 'Text\xa0/\xa0HTML', icon: 'ic_product_list' },
|
|
6
|
+
csv: { text: 'CSV', icon: 'ic_csv_file' },
|
|
7
|
+
json: { text: 'Json', icon: 'ic_file_attached_outline', developer: true },
|
|
8
|
+
};
|
|
9
|
+
export type CopyOptionsKey = keyof typeof gridCopyOptions;
|
|
10
|
+
|
|
11
|
+
const CopyOptionsStorageKey = 'stepAgGrid_defaultCopy';
|
|
12
|
+
|
|
13
|
+
export const useGridCopySettings = () => {
|
|
14
|
+
const [copyType, _setCopyType] = useState<CopyOptionsKey>(() => {
|
|
15
|
+
const defaultCopy = window.localStorage.getItem(CopyOptionsStorageKey) ?? '';
|
|
16
|
+
return Object.keys(gridCopyOptions).includes(defaultCopy) ? defaultCopy : 'html';
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const setCopyType = useCallback((key: CopyOptionsKey) => {
|
|
20
|
+
window.localStorage.setItem(CopyOptionsStorageKey, key);
|
|
21
|
+
_setCopyType(key);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
copyType,
|
|
26
|
+
setCopyType,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { CellMouseDownEvent, CellMouseOverEvent, IRowNode } from 'ag-grid-community';
|
|
2
|
+
import { MutableRefObject, RefObject, useCallback, useState } from 'react';
|
|
3
|
+
import { useInterval } from 'usehooks-ts';
|
|
4
|
+
|
|
5
|
+
import { GridBaseRow } from '../types';
|
|
6
|
+
|
|
7
|
+
export interface CellLocation {
|
|
8
|
+
rowId: string;
|
|
9
|
+
colId: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GridRanges<TData extends GridBaseRow> {
|
|
14
|
+
selectedColIds: string[];
|
|
15
|
+
selectedNodes: IRowNode<TData>[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const useGridRangeSelection = <TData extends GridBaseRow>({
|
|
19
|
+
enableRangeSelection,
|
|
20
|
+
gridDivRef,
|
|
21
|
+
rangeStartRef,
|
|
22
|
+
rangeEndRef,
|
|
23
|
+
hasSelectedMoreThanOneCellRef,
|
|
24
|
+
rangeSortedNodesRef,
|
|
25
|
+
}: {
|
|
26
|
+
enableRangeSelection: boolean;
|
|
27
|
+
gridDivRef: RefObject<HTMLDivElement>;
|
|
28
|
+
rangeStartRef: MutableRefObject<CellLocation | null>;
|
|
29
|
+
rangeEndRef: MutableRefObject<CellLocation | null>;
|
|
30
|
+
hasSelectedMoreThanOneCellRef: MutableRefObject<boolean>;
|
|
31
|
+
rangeSortedNodesRef: MutableRefObject<IRowNode<TData>[] | null>;
|
|
32
|
+
}) => {
|
|
33
|
+
const [refreshIntervalEnabled, setRefreshIntervalEnabled] = useState(false);
|
|
34
|
+
|
|
35
|
+
const ranges = useCallback((): GridRanges<TData> => {
|
|
36
|
+
const gridElement = gridDivRef.current!;
|
|
37
|
+
const rangeStart = rangeStartRef.current!;
|
|
38
|
+
const rangeEnd = rangeEndRef.current!;
|
|
39
|
+
const rangeSortedNodes = rangeSortedNodesRef.current!;
|
|
40
|
+
if (!gridElement || !rangeStart || !rangeEnd || !rangeSortedNodes) {
|
|
41
|
+
return {
|
|
42
|
+
selectedColIds: [],
|
|
43
|
+
selectedNodes: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const elStyleLeftComparator = (el1: Element, el2: Element) => elStyleLeft(el1) - elStyleLeft(el2);
|
|
48
|
+
const elStyleLeft = (el: Element): number => parseFloat((el as HTMLElement).style.left) ?? 0;
|
|
49
|
+
|
|
50
|
+
const getSortedColIds = (): string[] => {
|
|
51
|
+
//
|
|
52
|
+
const leftHeaders = [...gridElement.querySelectorAll('.ag-pinned-left-header .ag-header-cell')].sort(
|
|
53
|
+
elStyleLeftComparator,
|
|
54
|
+
);
|
|
55
|
+
const centerHeaders = [...gridElement.querySelectorAll('.ag-header-viewport .ag-header-cell')].sort(
|
|
56
|
+
elStyleLeftComparator,
|
|
57
|
+
);
|
|
58
|
+
const rightHeaders = [...gridElement.querySelectorAll('.ag-pinned-right-header .ag-header-cell')].sort(
|
|
59
|
+
elStyleLeftComparator,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return [...leftHeaders, ...centerHeaders, ...rightHeaders].map((el, i) => el.getAttribute('col-id') ?? String(i));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const sortedColIds = getSortedColIds();
|
|
66
|
+
|
|
67
|
+
const startColIndex = sortedColIds.indexOf(rangeStart.colId);
|
|
68
|
+
const endColIndex = sortedColIds.indexOf(rangeEnd.colId);
|
|
69
|
+
const selectedColIds = sortedColIds.slice(
|
|
70
|
+
Math.min(startColIndex, endColIndex),
|
|
71
|
+
Math.max(startColIndex, endColIndex) + 1,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const startRowIndex = rangeSortedNodes.findIndex((node) => node.data!.id === rangeStart.rowId);
|
|
75
|
+
const endRowIndex = rangeSortedNodes.findIndex((node) => node.data!.id === rangeEnd.rowId);
|
|
76
|
+
const selectedNodes = rangeSortedNodes.slice(
|
|
77
|
+
Math.min(startRowIndex, endRowIndex),
|
|
78
|
+
Math.max(startRowIndex, endRowIndex) + 1,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return { selectedColIds, selectedNodes };
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const redrawSelectedRanges = useCallback(() => {
|
|
85
|
+
const gridElement = gridDivRef.current!;
|
|
86
|
+
const { selectedColIds, selectedNodes } = ranges();
|
|
87
|
+
|
|
88
|
+
selectedColIds.forEach((colId, colIndex) => {
|
|
89
|
+
selectedNodes.forEach((node, rowIndex) => {
|
|
90
|
+
const rowId = node.data!.id;
|
|
91
|
+
const cell = gridElement.querySelector(
|
|
92
|
+
`.ag-row[row-id=${JSON.stringify(String(rowId))}] .ag-cell[col-id=${JSON.stringify(colId)}`,
|
|
93
|
+
);
|
|
94
|
+
cell?.classList.add('rangeSelect');
|
|
95
|
+
if (colIndex === 0) {
|
|
96
|
+
cell?.classList.add('rangeSelectLeft');
|
|
97
|
+
}
|
|
98
|
+
if (colIndex === selectedColIds.length - 1) {
|
|
99
|
+
cell?.classList.add('rangeSelectRight');
|
|
100
|
+
}
|
|
101
|
+
if (rowIndex === 0) {
|
|
102
|
+
cell?.classList.add('rangeSelectTop');
|
|
103
|
+
}
|
|
104
|
+
if (rowIndex === selectedNodes.length - 1) {
|
|
105
|
+
cell?.classList.add('rangeSelectBottom');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const updateRangeSelectionCellClasses = useCallback(
|
|
112
|
+
(justRefresh?: boolean) => {
|
|
113
|
+
//
|
|
114
|
+
// Get all grid cols, sort by pinned, then style: left
|
|
115
|
+
const gridElement = gridDivRef.current;
|
|
116
|
+
if (!gridElement) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Clear all selections
|
|
121
|
+
if (!justRefresh) {
|
|
122
|
+
gridElement
|
|
123
|
+
.querySelectorAll('.rangeSelect,.rangeSelectLeft,.rangeSelectTop,.rangeSelectRight,.rangeSelectBottom')
|
|
124
|
+
.forEach((el) => {
|
|
125
|
+
el.classList.remove(
|
|
126
|
+
'rangeSelect',
|
|
127
|
+
'rangeSelectLeft',
|
|
128
|
+
'rangeSelectTop',
|
|
129
|
+
'rangeSelectRight',
|
|
130
|
+
'rangeSelectBottom',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// if range selection multiple add .Grid-container.rangeSelectingMultiple
|
|
136
|
+
const rangeStart = rangeStartRef.current;
|
|
137
|
+
const rangeEnd = rangeEndRef.current;
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
rangeStart !== null &&
|
|
141
|
+
rangeEnd !== null &&
|
|
142
|
+
(hasSelectedMoreThanOneCellRef.current ||
|
|
143
|
+
rangeStart.colId !== rangeEnd.colId ||
|
|
144
|
+
rangeStart.rowId !== rangeEnd.rowId)
|
|
145
|
+
) {
|
|
146
|
+
gridElement.classList.add('rangeSelectingMultiple');
|
|
147
|
+
gridElement.querySelectorAll('.ag-cell-focus').forEach((el) => {
|
|
148
|
+
el.classList.remove('ag-cell-focus');
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
gridElement.classList.remove('rangeSelectingMultiple');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const rangeSortedNodes = rangeSortedNodesRef.current;
|
|
155
|
+
if (!rangeSortedNodes) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
redrawSelectedRanges();
|
|
160
|
+
},
|
|
161
|
+
[ranges, redrawSelectedRanges],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Handle updates after scroll / grid refresh
|
|
165
|
+
useInterval(
|
|
166
|
+
() => {
|
|
167
|
+
updateRangeSelectionCellClasses(true);
|
|
168
|
+
},
|
|
169
|
+
refreshIntervalEnabled ? 150 : null,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const clearRangeSelection = useCallback(() => {
|
|
173
|
+
hasSelectedMoreThanOneCellRef.current = false;
|
|
174
|
+
setRefreshIntervalEnabled(false);
|
|
175
|
+
|
|
176
|
+
hasSelectedMoreThanOneCellRef.current = false;
|
|
177
|
+
rangeStartRef.current = null;
|
|
178
|
+
rangeEndRef.current = null;
|
|
179
|
+
|
|
180
|
+
updateRangeSelectionCellClasses();
|
|
181
|
+
}, [updateRangeSelectionCellClasses]);
|
|
182
|
+
|
|
183
|
+
const onCellMouseOver = useCallback(
|
|
184
|
+
(e: CellMouseOverEvent, mouseDown?: boolean) => {
|
|
185
|
+
if (!enableRangeSelection) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const button = (e.event as { buttons?: number }).buttons;
|
|
189
|
+
if (button !== 1) {
|
|
190
|
+
// TODO fix this?
|
|
191
|
+
//rangeSortedNodesRef.current = null;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
rangeEndRef.current = {
|
|
195
|
+
rowId: e.node.data.id,
|
|
196
|
+
colId: e.column.getColId(),
|
|
197
|
+
timestamp: Date.now(),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (mouseDown) {
|
|
201
|
+
const sortedNodes: IRowNode<TData>[] = [];
|
|
202
|
+
e.api.forEachNodeAfterFilterAndSort((node: IRowNode<TData>) => sortedNodes.push(node));
|
|
203
|
+
rangeSortedNodesRef.current = sortedNodes;
|
|
204
|
+
setRefreshIntervalEnabled(true);
|
|
205
|
+
rangeStartRef.current = { ...rangeEndRef.current };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (
|
|
209
|
+
rangeStartRef.current &&
|
|
210
|
+
rangeEndRef.current &&
|
|
211
|
+
(rangeStartRef.current.rowId !== rangeEndRef.current.rowId ||
|
|
212
|
+
rangeStartRef.current.colId !== rangeEndRef.current.colId)
|
|
213
|
+
) {
|
|
214
|
+
hasSelectedMoreThanOneCellRef.current = true;
|
|
215
|
+
window.getSelection()?.removeAllRanges();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
updateRangeSelectionCellClasses();
|
|
219
|
+
},
|
|
220
|
+
[enableRangeSelection, updateRangeSelectionCellClasses],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const onCellMouseDown = useCallback(
|
|
224
|
+
(e: CellMouseDownEvent) => {
|
|
225
|
+
const button = (e.event as { buttons?: number }).buttons;
|
|
226
|
+
if (button === 1) {
|
|
227
|
+
clearRangeSelection();
|
|
228
|
+
}
|
|
229
|
+
onCellMouseOver(e as unknown as CellMouseOverEvent, true);
|
|
230
|
+
},
|
|
231
|
+
[onCellMouseOver],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return { clearRangeSelection, ranges, onCellMouseDown, onCellMouseOver };
|
|
235
|
+
};
|
|
@@ -11,6 +11,9 @@ import { GridFormEditBearing, GridFormEditBearingProps } from '../gridForm/GridF
|
|
|
11
11
|
import { GenericCellColDef } from '../gridRender/GridRenderGenericCell';
|
|
12
12
|
import { ColDefT, GridBaseRow } from '../types';
|
|
13
13
|
|
|
14
|
+
export const GridCellBearingValueFormatter = (params: ValueFormatterParams) =>
|
|
15
|
+
bearingValueFormatter(params.value) ?? '';
|
|
16
|
+
|
|
14
17
|
const GridPopoverEditBearingLike = <TData extends GridBaseRow, TValue = any>(
|
|
15
18
|
colDef: GenericCellColDef<TData, TValue>,
|
|
16
19
|
props: GenericCellEditorProps<GridFormEditBearingProps<TData>> & {
|
package/src/components/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ColDef, EditableCallback, ICellRendererParams, ValueFormatterFunc, ValueGetterFunc } from 'ag-grid-community';
|
|
2
2
|
import { ReactElement } from 'react';
|
|
3
3
|
|
|
4
|
+
export const agGridSelectRowColId = 'ag-Grid-SelectionColumn';
|
|
5
|
+
|
|
4
6
|
export interface GridBaseRow {
|
|
5
7
|
id: string | number;
|
|
6
8
|
}
|
|
@@ -70,6 +70,7 @@ export interface GridContextType<TData extends GridBaseRow> {
|
|
|
70
70
|
onBulkEditingComplete: () => Promise<void> | void;
|
|
71
71
|
setOnBulkEditingComplete: (callback: (() => Promise<void> | void) | undefined) => void;
|
|
72
72
|
showNoRowsOverlay: () => void;
|
|
73
|
+
getCellValue: GridApi['getCellValue'];
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
const NoContext = <T,>(): T => {
|
|
@@ -80,6 +81,7 @@ const NoContext = <T,>(): T => {
|
|
|
80
81
|
export const GridContext = createContext<GridContextType<any>>({
|
|
81
82
|
gridReady: false,
|
|
82
83
|
gridRenderState: () => null,
|
|
84
|
+
getCellValue: NoContext,
|
|
83
85
|
getColDef: NoContext,
|
|
84
86
|
getColumns: NoContext,
|
|
85
87
|
getColumnIds: NoContext,
|
|
@@ -13,7 +13,7 @@ import debounce from 'debounce-promise';
|
|
|
13
13
|
import { compact, defer, delay, difference, filter, isEmpty, last, pull, remove, sortBy, sumBy } from 'lodash-es';
|
|
14
14
|
import { PropsWithChildren, ReactElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
|
|
16
|
-
import { ColDefT, GridBaseRow } from '../components';
|
|
16
|
+
import { agGridSelectRowColId, ColDefT, GridBaseRow } from '../components';
|
|
17
17
|
import { GridCellFillerColId, isGridCellFiller } from '../components/GridCellFiller';
|
|
18
18
|
import { getColId, isFlexColumn } from '../components/gridUtil';
|
|
19
19
|
import { fnOrVar, isNotEmpty, sanitiseFileName, wait } from '../utils/util';
|
|
@@ -83,6 +83,8 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
83
83
|
[gridApi],
|
|
84
84
|
);
|
|
85
85
|
|
|
86
|
+
const getCellValue = useCallback<GridApi['getCellValue']>((...props) => gridApi?.getCellValue(...props), [gridApi]);
|
|
87
|
+
|
|
86
88
|
/**
|
|
87
89
|
* Scroll row into view by Id.
|
|
88
90
|
*
|
|
@@ -302,7 +304,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
302
304
|
// It may be that the first cell is the selection cell, this doesn't exist as a colDef
|
|
303
305
|
// so instead, I just try and select it. If it doesn't exist selection will stay on the
|
|
304
306
|
// previously focused cell
|
|
305
|
-
gridApi.setFocusedCell(rowIndex,
|
|
307
|
+
gridApi.setFocusedCell(rowIndex, agGridSelectRowColId);
|
|
306
308
|
}
|
|
307
309
|
}, 100);
|
|
308
310
|
}
|
|
@@ -902,6 +904,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
902
904
|
getSelectedRowIds,
|
|
903
905
|
getFilteredSelectedRowIds,
|
|
904
906
|
getFirstRowId,
|
|
907
|
+
getCellValue,
|
|
905
908
|
editingCells,
|
|
906
909
|
ensureRowVisible,
|
|
907
910
|
ensureSelectedRowIsVisible,
|