@azrico/reactgrid 1.6.0 → 1.6.2
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 -10
- package/dist/src/example.js +2 -0
- package/dist/src/hooks/useGridEvents.js +4 -3
- package/dist/src/hooks/useGridSearch.js +29 -4
- package/dist/src/types/types.d.ts +4 -0
- package/package.json +67 -67
- package/src/example.tsx +186 -184
- package/src/hooks/useGridEvents.ts +158 -157
- package/src/hooks/useGridSearch.tsx +146 -106
- package/src/types/types.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
|
-
|
|
5
|
-
### 1.
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
|
+
|
|
5
|
+
### 1.7.1 (2026-10-14)
|
|
6
|
+
|
|
7
|
+
* Fix two quick clicks on different rows firing `onRowDoubleClick`: the double-click check now keys clicks by the row's id instead of `undefined`.
|
|
8
|
+
|
|
9
|
+
### 1.7.0 (2026-10-14)
|
|
10
|
+
|
|
11
|
+
* Add `advancedFilterLabel` to rename the quick-search label (default "Search").
|
|
12
|
+
* Add `advancedFilterPlaceholder` for the quick-search input. A string is the native placeholder; any other node (custom HTML) is shown over the empty input.
|
|
13
|
+
|
|
14
|
+
### 1.6.0 (2026-10-14)
|
|
15
|
+
|
|
16
|
+
* Pass `filterValue`, `sortInfo` and `groupBy` to a `dataSource` function next to `skip`, `limit`, `cached_data` and `last_cache_info`, so header filters can drive a server-side search.
|
|
17
|
+
* With `gridOptions.remoteFilter` / `remoteSort`, a filter or sort change invalidates the data cache and calls `dataSource` again. Local filtering and sorting still reuse the cached rows.
|
|
18
|
+
* Export the typed `AzDataSourceParams` and `AzDataSourceFunc` for the `dataSource` and `details_dataSource` props.
|
|
19
|
+
* Re-export `SelectFilter`, `NumberFilter`, `DateFilter` and `BoolFilter` for `column.filterEditor`.
|
package/dist/src/example.js
CHANGED
|
@@ -175,6 +175,8 @@ function ExampleGrid() {
|
|
|
175
175
|
editable: true,
|
|
176
176
|
onEditComplete: onEditComplete,
|
|
177
177
|
advancedFilterFields: ['name'],
|
|
178
|
+
advancedFilterLabel: "Search Name",
|
|
179
|
+
advancedFilterPlaceholder: _react.default.createElement("i", null, "Type a name"),
|
|
178
180
|
// leftColumns={leftColumns}
|
|
179
181
|
gridOptions: {
|
|
180
182
|
defaultFilterValue: [{
|
|
@@ -60,15 +60,16 @@ function useGridEvents(grid, props) {
|
|
|
60
60
|
onRowDoubleClick
|
|
61
61
|
} = latestProps.current;
|
|
62
62
|
const nowDate = Date.now();
|
|
63
|
-
const
|
|
63
|
+
const rowId = (0, _func_grid.get_row_id)(rowProps.data);
|
|
64
|
+
const lastclick = click_data.current[rowId];
|
|
64
65
|
if (lastclick) {
|
|
65
66
|
const diffTime = Math.abs(nowDate - lastclick);
|
|
66
67
|
if (diffTime < 400) {
|
|
67
|
-
click_data.current[
|
|
68
|
+
click_data.current[rowId] = null;
|
|
68
69
|
return onRowDoubleClick && onRowDoubleClick(rowProps.rowIndex, rowProps.data);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
|
-
click_data.current[
|
|
72
|
+
click_data.current[rowId] = nowDate;
|
|
72
73
|
return onRowClick && onRowClick(rowProps.rowIndex, rowProps.data);
|
|
73
74
|
}, []);
|
|
74
75
|
_react.default.useEffect(() => {
|
|
@@ -79,14 +79,39 @@ function useGridSearch(props) {
|
|
|
79
79
|
searchId.current++;
|
|
80
80
|
set_searchResult(null);
|
|
81
81
|
}, [search, props.dataSource]);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const {
|
|
83
|
+
advancedFilterLabel,
|
|
84
|
+
advancedFilterPlaceholder
|
|
85
|
+
} = props;
|
|
86
|
+
const textPlaceholder = typeof advancedFilterPlaceholder === 'string' || typeof advancedFilterPlaceholder === 'number' ? String(advancedFilterPlaceholder) : undefined;
|
|
87
|
+
// custom HTML cannot go in the native placeholder attribute, so it is shown over the empty input
|
|
88
|
+
const nodePlaceholder = textPlaceholder === undefined && advancedFilterPlaceholder !== undefined && advancedFilterPlaceholder !== null && typeof advancedFilterPlaceholder !== 'boolean' ? advancedFilterPlaceholder : undefined;
|
|
89
|
+
const input = _react.default.createElement("input", {
|
|
85
90
|
type: "text",
|
|
86
91
|
name: "name",
|
|
87
92
|
value: search,
|
|
93
|
+
placeholder: textPlaceholder,
|
|
88
94
|
onChange: e => set_search(e.target.value)
|
|
89
|
-
})
|
|
95
|
+
});
|
|
96
|
+
const element = _react.default.createElement(_react.default.Fragment, null, _react.default.createElement("form", {
|
|
97
|
+
className: 'azgrid-searcharea'
|
|
98
|
+
}, _react.default.createElement("label", null, advancedFilterLabel !== null && advancedFilterLabel !== void 0 ? advancedFilterLabel : 'Search', nodePlaceholder === undefined ? input : _react.default.createElement("span", {
|
|
99
|
+
style: {
|
|
100
|
+
position: 'relative',
|
|
101
|
+
display: 'inline-block'
|
|
102
|
+
}
|
|
103
|
+
}, input, search ? null : _react.default.createElement("span", {
|
|
104
|
+
style: {
|
|
105
|
+
position: 'absolute',
|
|
106
|
+
left: 8,
|
|
107
|
+
top: 0,
|
|
108
|
+
bottom: 0,
|
|
109
|
+
display: 'flex',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
pointerEvents: 'none',
|
|
112
|
+
opacity: 0.6
|
|
113
|
+
}
|
|
114
|
+
}, nodePlaceholder)))));
|
|
90
115
|
return {
|
|
91
116
|
filteredData: searchResult !== null && searchResult !== void 0 ? searchResult : trackedDataSource,
|
|
92
117
|
element
|
|
@@ -88,6 +88,10 @@ export type AzGridProps = {
|
|
|
88
88
|
limit?: number;
|
|
89
89
|
editable?: boolean | ((editValue: any, cellProps: any) => boolean);
|
|
90
90
|
advancedFilterFields?: string[];
|
|
91
|
+
/** Label of the quick-search box shown with `advancedFilterFields`. Default "Search". */
|
|
92
|
+
advancedFilterLabel?: React.ReactNode;
|
|
93
|
+
/** Placeholder of the quick-search input. A string is used as the native placeholder, any other node (custom HTML) is shown over the empty input. */
|
|
94
|
+
advancedFilterPlaceholder?: React.ReactNode;
|
|
91
95
|
loading?: boolean;
|
|
92
96
|
theme?: 'light' | 'dark' | (string & {});
|
|
93
97
|
/** @deprecated Use theme="dark" or theme="light" instead. */
|
package/package.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@azrico/reactgrid",
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"description": "React Datagrid",
|
|
6
|
-
"author": "Azrideus <MehrdadTavangar@gmail.com>",
|
|
7
|
-
"homepage": ".",
|
|
8
|
-
"scripts": {
|
|
9
|
-
"start": "react-scripts start",
|
|
10
|
-
"build": "tsc && yarn cp_themes && babel dist --out-dir dist",
|
|
11
|
-
"test": "rem",
|
|
12
|
-
"check:theme": "node config/check-theme.cjs",
|
|
13
|
-
"theme:generate": "node config/generate-theme.cjs",
|
|
14
|
-
"cp_themes": "copyfiles ./src/themes/* ./dist/"
|
|
15
|
-
},
|
|
16
|
-
"config-overrides-path": "../../config/config-overrides.js",
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@babel/cli": "^7.13.14",
|
|
19
|
-
"@babel/core": "^7.13.15",
|
|
20
|
-
"@babel/eslint-parser": "^7.12.17",
|
|
21
|
-
"@babel/node": "^7.13.13",
|
|
22
|
-
"@babel/plugin-proposal-class-properties": "^7.12.13",
|
|
23
|
-
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
|
24
|
-
"@babel/preset-env": "^7.13.15",
|
|
25
|
-
"@babel/preset-typescript": "^7.14.5",
|
|
26
|
-
"@mui/material": "*",
|
|
27
|
-
"@types/react": "^18.0.25",
|
|
28
|
-
"@types/react-dom": "^18.0.8",
|
|
29
|
-
"react-dom": "^18.2.0",
|
|
30
|
-
"react-scripts": "^5.0.1",
|
|
31
|
-
"typescript": "^4.8.4"
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"@azrico/controls": "1.6.1",
|
|
35
|
-
"@azrico/object": "^1.4.97",
|
|
36
|
-
"@emotion/react": "^11.9.3",
|
|
37
|
-
"@emotion/styled": "^11.9.3",
|
|
38
|
-
"@inovua/reactdatagrid-enterprise": "^5.10.2",
|
|
39
|
-
"clsx": "^2.0.0",
|
|
40
|
-
"lodash.debounce": "^4.0.8",
|
|
41
|
-
"react": "^18.2.0",
|
|
42
|
-
"react-use-size": "^3.0.1"
|
|
43
|
-
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"@azrico/controls": ">=1.6.1",
|
|
46
|
-
"@azrico/object": ">=1.4.97",
|
|
47
|
-
"@emotion/react": ">=11.9.3",
|
|
48
|
-
"@emotion/styled": ">=11.9.3",
|
|
49
|
-
"@inovua/reactdatagrid-enterprise": ">=5.10.2",
|
|
50
|
-
"@mui/material": ">=5.0.0 <8.0.0",
|
|
51
|
-
"clsx": ">=1.2.1",
|
|
52
|
-
"react": ">=18.2.0",
|
|
53
|
-
"react-use-size": ">=3.0.1"
|
|
54
|
-
},
|
|
55
|
-
"browserslist": {
|
|
56
|
-
"production": [
|
|
57
|
-
">0.2%",
|
|
58
|
-
"not dead",
|
|
59
|
-
"not op_mini all"
|
|
60
|
-
],
|
|
61
|
-
"development": [
|
|
62
|
-
"last 1 chrome version",
|
|
63
|
-
"last 1 firefox version",
|
|
64
|
-
"last 1 safari version"
|
|
65
|
-
]
|
|
66
|
-
},
|
|
67
|
-
"packageManager": "yarn@4.10.2"
|
|
1
|
+
{
|
|
2
|
+
"name": "@azrico/reactgrid",
|
|
3
|
+
"version": "1.6.2",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"description": "React Datagrid",
|
|
6
|
+
"author": "Azrideus <MehrdadTavangar@gmail.com>",
|
|
7
|
+
"homepage": ".",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "react-scripts start",
|
|
10
|
+
"build": "tsc && yarn cp_themes && babel dist --out-dir dist",
|
|
11
|
+
"test": "rem",
|
|
12
|
+
"check:theme": "node config/check-theme.cjs",
|
|
13
|
+
"theme:generate": "node config/generate-theme.cjs",
|
|
14
|
+
"cp_themes": "copyfiles ./src/themes/* ./dist/"
|
|
15
|
+
},
|
|
16
|
+
"config-overrides-path": "../../config/config-overrides.js",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@babel/cli": "^7.13.14",
|
|
19
|
+
"@babel/core": "^7.13.15",
|
|
20
|
+
"@babel/eslint-parser": "^7.12.17",
|
|
21
|
+
"@babel/node": "^7.13.13",
|
|
22
|
+
"@babel/plugin-proposal-class-properties": "^7.12.13",
|
|
23
|
+
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
|
24
|
+
"@babel/preset-env": "^7.13.15",
|
|
25
|
+
"@babel/preset-typescript": "^7.14.5",
|
|
26
|
+
"@mui/material": "*",
|
|
27
|
+
"@types/react": "^18.0.25",
|
|
28
|
+
"@types/react-dom": "^18.0.8",
|
|
29
|
+
"react-dom": "^18.2.0",
|
|
30
|
+
"react-scripts": "^5.0.1",
|
|
31
|
+
"typescript": "^4.8.4"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@azrico/controls": "1.6.1",
|
|
35
|
+
"@azrico/object": "^1.4.97",
|
|
36
|
+
"@emotion/react": "^11.9.3",
|
|
37
|
+
"@emotion/styled": "^11.9.3",
|
|
38
|
+
"@inovua/reactdatagrid-enterprise": "^5.10.2",
|
|
39
|
+
"clsx": "^2.0.0",
|
|
40
|
+
"lodash.debounce": "^4.0.8",
|
|
41
|
+
"react": "^18.2.0",
|
|
42
|
+
"react-use-size": "^3.0.1"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@azrico/controls": ">=1.6.1",
|
|
46
|
+
"@azrico/object": ">=1.4.97",
|
|
47
|
+
"@emotion/react": ">=11.9.3",
|
|
48
|
+
"@emotion/styled": ">=11.9.3",
|
|
49
|
+
"@inovua/reactdatagrid-enterprise": ">=5.10.2",
|
|
50
|
+
"@mui/material": ">=5.0.0 <8.0.0",
|
|
51
|
+
"clsx": ">=1.2.1",
|
|
52
|
+
"react": ">=18.2.0",
|
|
53
|
+
"react-use-size": ">=3.0.1"
|
|
54
|
+
},
|
|
55
|
+
"browserslist": {
|
|
56
|
+
"production": [
|
|
57
|
+
">0.2%",
|
|
58
|
+
"not dead",
|
|
59
|
+
"not op_mini all"
|
|
60
|
+
],
|
|
61
|
+
"development": [
|
|
62
|
+
"last 1 chrome version",
|
|
63
|
+
"last 1 firefox version",
|
|
64
|
+
"last 1 safari version"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"packageManager": "yarn@4.10.2"
|
|
68
68
|
}
|
package/src/example.tsx
CHANGED
|
@@ -1,184 +1,186 @@
|
|
|
1
|
-
export * from './views/DataGrid';
|
|
2
|
-
|
|
3
|
-
import AzGrid from './views/DataGrid';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
import './themes/azgrid-theme.css';
|
|
6
|
-
import './themes/example-palettes.css';
|
|
7
|
-
|
|
8
|
-
const columns = [
|
|
9
|
-
{
|
|
10
|
-
name: 'name',
|
|
11
|
-
title: 'Name',
|
|
12
|
-
defaultFlex: 1,
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: 'job',
|
|
16
|
-
title: 'Job',
|
|
17
|
-
type: 'combo',
|
|
18
|
-
items: ['None', 'Engineer', 'Doctor'],
|
|
19
|
-
defaultFlex: 1,
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
name: 'age',
|
|
23
|
-
title: 'Age',
|
|
24
|
-
type: 'number',
|
|
25
|
-
defaultFlex: 1,
|
|
26
|
-
ariaLabel: (value: any) => `${value} years`,
|
|
27
|
-
},
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
const editableColumns = ['job', 'age'];
|
|
31
|
-
|
|
32
|
-
function getData(delta: any, max = 200) {
|
|
33
|
-
const fd: any[] = [];
|
|
34
|
-
for (let index = 0; index < max; index++) {
|
|
35
|
-
const currentObject = {
|
|
36
|
-
__table_id: String(index),
|
|
37
|
-
name: 'user-' + index,
|
|
38
|
-
age: 5 + index,
|
|
39
|
-
job: 'None',
|
|
40
|
-
};
|
|
41
|
-
if (delta[index] && typeof delta[index] === 'object') {
|
|
42
|
-
for (const key in delta[index]) {
|
|
43
|
-
currentObject[key] = delta[index][key];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const childrows: any[] = [];
|
|
48
|
-
if (index % 5 == 0) {
|
|
49
|
-
const sub_selfid = 's' + index;
|
|
50
|
-
childrows.push({
|
|
51
|
-
__table_id: sub_selfid,
|
|
52
|
-
__parent__table_id: String(index),
|
|
53
|
-
name: 'sub-' + index,
|
|
54
|
-
age: 20,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
currentObject['__children'] = childrows;
|
|
59
|
-
fd.push(currentObject);
|
|
60
|
-
}
|
|
61
|
-
return fd;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function ExampleGrid() {
|
|
65
|
-
const [ds_reset, set_ds_reset] = React.useState(0);
|
|
66
|
-
const [delta, set_delta] = React.useState({});
|
|
67
|
-
const [delayed_reload, set_delayed_reload] = React.useState(false);
|
|
68
|
-
|
|
69
|
-
const dataSource = React.useMemo(() => getData(delta), []);
|
|
70
|
-
// const dataSource = React.useCallback(
|
|
71
|
-
// async (dt) => {
|
|
72
|
-
// //console.log('Start loading');
|
|
73
|
-
// return await new Promise((resolve) => {
|
|
74
|
-
// setTimeout(() => {
|
|
75
|
-
// resolve(getData(delta));
|
|
76
|
-
// }, 1000);
|
|
77
|
-
// });
|
|
78
|
-
// },
|
|
79
|
-
// [ds_reset, delta]
|
|
80
|
-
// );
|
|
81
|
-
// const details_dataSource = React.useCallback(
|
|
82
|
-
// async (dt) => {
|
|
83
|
-
// //console.log('Start loading');
|
|
84
|
-
// return await new Promise((resolve) => {
|
|
85
|
-
// setTimeout(() => {
|
|
86
|
-
// resolve(getData(delta, 5));
|
|
87
|
-
// }, 1000);
|
|
88
|
-
// });
|
|
89
|
-
// },
|
|
90
|
-
// [ds_reset, delta]
|
|
91
|
-
// );
|
|
92
|
-
|
|
93
|
-
function onEditComplete(params) {
|
|
94
|
-
const { value, columnId, rowId, ...rest } = params;
|
|
95
|
-
let new_rd = delta[rowId];
|
|
96
|
-
if (!new_rd) new_rd = {};
|
|
97
|
-
new_rd[columnId] = value;
|
|
98
|
-
set_delta((d) => {
|
|
99
|
-
return { ...d, [rowId]: new_rd };
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const onRowDoubleClick = React.useCallback((index: number, rowdata: any) => {
|
|
104
|
-
console.log('dbl', rowdata?.data || rowdata);
|
|
105
|
-
alert('dbl: ' + index);
|
|
106
|
-
}, []);
|
|
107
|
-
const onRowClick = React.useCallback((index: number, rowdata: any) => {
|
|
108
|
-
console.log('click:', rowdata?.data || rowdata);
|
|
109
|
-
}, []);
|
|
110
|
-
|
|
111
|
-
React.useEffect(() => {
|
|
112
|
-
setTimeout(() => {
|
|
113
|
-
set_delayed_reload(true);
|
|
114
|
-
}, 2000);
|
|
115
|
-
}, []);
|
|
116
|
-
const customColumns = React.useMemo(() => {
|
|
117
|
-
if (!delayed_reload) return [];
|
|
118
|
-
return [{ name: 'name', render: (r) => r.value + '^' }];
|
|
119
|
-
}, [delayed_reload]);
|
|
120
|
-
|
|
121
|
-
const contextMenuItems = React.useCallback((product: any) => {
|
|
122
|
-
return [
|
|
123
|
-
{
|
|
124
|
-
type: 'text',
|
|
125
|
-
content: product.name,
|
|
126
|
-
},
|
|
127
|
-
];
|
|
128
|
-
}, []);
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<div style={{ display: 'grid', gap: 20, height: '100%', overflow: 'auto' }}>
|
|
132
|
-
{['light', 'dark', 'deco'].map((theme) => (
|
|
133
|
-
<section key={theme} className={theme === 'deco' ? 'azgrid-example-deco' : undefined} style={{ height: 360, display: 'flex', flexDirection: 'column' }}>
|
|
134
|
-
<h3>{theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`}</h3>
|
|
135
|
-
<AzGrid
|
|
136
|
-
theme={theme}
|
|
137
|
-
ariaLabel={`Users (${theme})`}
|
|
138
|
-
delayedSize
|
|
139
|
-
// details_dataSource={details_dataSource}
|
|
140
|
-
dataSource={dataSource}
|
|
141
|
-
details_columns={columns}
|
|
142
|
-
onRowClick={onRowClick}
|
|
143
|
-
onRowDoubleClick={onRowDoubleClick}
|
|
144
|
-
allowChildRows
|
|
145
|
-
// onSelectionChange={onSelectionChange}
|
|
146
|
-
// selected={selected}
|
|
147
|
-
contextMenuItems={contextMenuItems as any}
|
|
148
|
-
customColumns={customColumns}
|
|
149
|
-
columns={columns}
|
|
150
|
-
editableColumns={editableColumns}
|
|
151
|
-
editable={true}
|
|
152
|
-
onEditComplete={onEditComplete}
|
|
153
|
-
advancedFilterFields={['name']}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
1
|
+
export * from './views/DataGrid';
|
|
2
|
+
|
|
3
|
+
import AzGrid from './views/DataGrid';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import './themes/azgrid-theme.css';
|
|
6
|
+
import './themes/example-palettes.css';
|
|
7
|
+
|
|
8
|
+
const columns = [
|
|
9
|
+
{
|
|
10
|
+
name: 'name',
|
|
11
|
+
title: 'Name',
|
|
12
|
+
defaultFlex: 1,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'job',
|
|
16
|
+
title: 'Job',
|
|
17
|
+
type: 'combo',
|
|
18
|
+
items: ['None', 'Engineer', 'Doctor'],
|
|
19
|
+
defaultFlex: 1,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'age',
|
|
23
|
+
title: 'Age',
|
|
24
|
+
type: 'number',
|
|
25
|
+
defaultFlex: 1,
|
|
26
|
+
ariaLabel: (value: any) => `${value} years`,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const editableColumns = ['job', 'age'];
|
|
31
|
+
|
|
32
|
+
function getData(delta: any, max = 200) {
|
|
33
|
+
const fd: any[] = [];
|
|
34
|
+
for (let index = 0; index < max; index++) {
|
|
35
|
+
const currentObject = {
|
|
36
|
+
__table_id: String(index),
|
|
37
|
+
name: 'user-' + index,
|
|
38
|
+
age: 5 + index,
|
|
39
|
+
job: 'None',
|
|
40
|
+
};
|
|
41
|
+
if (delta[index] && typeof delta[index] === 'object') {
|
|
42
|
+
for (const key in delta[index]) {
|
|
43
|
+
currentObject[key] = delta[index][key];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const childrows: any[] = [];
|
|
48
|
+
if (index % 5 == 0) {
|
|
49
|
+
const sub_selfid = 's' + index;
|
|
50
|
+
childrows.push({
|
|
51
|
+
__table_id: sub_selfid,
|
|
52
|
+
__parent__table_id: String(index),
|
|
53
|
+
name: 'sub-' + index,
|
|
54
|
+
age: 20,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
currentObject['__children'] = childrows;
|
|
59
|
+
fd.push(currentObject);
|
|
60
|
+
}
|
|
61
|
+
return fd;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ExampleGrid() {
|
|
65
|
+
const [ds_reset, set_ds_reset] = React.useState(0);
|
|
66
|
+
const [delta, set_delta] = React.useState({});
|
|
67
|
+
const [delayed_reload, set_delayed_reload] = React.useState(false);
|
|
68
|
+
|
|
69
|
+
const dataSource = React.useMemo(() => getData(delta), []);
|
|
70
|
+
// const dataSource = React.useCallback(
|
|
71
|
+
// async (dt) => {
|
|
72
|
+
// //console.log('Start loading');
|
|
73
|
+
// return await new Promise((resolve) => {
|
|
74
|
+
// setTimeout(() => {
|
|
75
|
+
// resolve(getData(delta));
|
|
76
|
+
// }, 1000);
|
|
77
|
+
// });
|
|
78
|
+
// },
|
|
79
|
+
// [ds_reset, delta]
|
|
80
|
+
// );
|
|
81
|
+
// const details_dataSource = React.useCallback(
|
|
82
|
+
// async (dt) => {
|
|
83
|
+
// //console.log('Start loading');
|
|
84
|
+
// return await new Promise((resolve) => {
|
|
85
|
+
// setTimeout(() => {
|
|
86
|
+
// resolve(getData(delta, 5));
|
|
87
|
+
// }, 1000);
|
|
88
|
+
// });
|
|
89
|
+
// },
|
|
90
|
+
// [ds_reset, delta]
|
|
91
|
+
// );
|
|
92
|
+
|
|
93
|
+
function onEditComplete(params) {
|
|
94
|
+
const { value, columnId, rowId, ...rest } = params;
|
|
95
|
+
let new_rd = delta[rowId];
|
|
96
|
+
if (!new_rd) new_rd = {};
|
|
97
|
+
new_rd[columnId] = value;
|
|
98
|
+
set_delta((d) => {
|
|
99
|
+
return { ...d, [rowId]: new_rd };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const onRowDoubleClick = React.useCallback((index: number, rowdata: any) => {
|
|
104
|
+
console.log('dbl', rowdata?.data || rowdata);
|
|
105
|
+
alert('dbl: ' + index);
|
|
106
|
+
}, []);
|
|
107
|
+
const onRowClick = React.useCallback((index: number, rowdata: any) => {
|
|
108
|
+
console.log('click:', rowdata?.data || rowdata);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
set_delayed_reload(true);
|
|
114
|
+
}, 2000);
|
|
115
|
+
}, []);
|
|
116
|
+
const customColumns = React.useMemo(() => {
|
|
117
|
+
if (!delayed_reload) return [];
|
|
118
|
+
return [{ name: 'name', render: (r) => r.value + '^' }];
|
|
119
|
+
}, [delayed_reload]);
|
|
120
|
+
|
|
121
|
+
const contextMenuItems = React.useCallback((product: any) => {
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
content: product.name,
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div style={{ display: 'grid', gap: 20, height: '100%', overflow: 'auto' }}>
|
|
132
|
+
{['light', 'dark', 'deco'].map((theme) => (
|
|
133
|
+
<section key={theme} className={theme === 'deco' ? 'azgrid-example-deco' : undefined} style={{ height: 360, display: 'flex', flexDirection: 'column' }}>
|
|
134
|
+
<h3>{theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`}</h3>
|
|
135
|
+
<AzGrid
|
|
136
|
+
theme={theme}
|
|
137
|
+
ariaLabel={`Users (${theme})`}
|
|
138
|
+
delayedSize
|
|
139
|
+
// details_dataSource={details_dataSource}
|
|
140
|
+
dataSource={dataSource}
|
|
141
|
+
details_columns={columns}
|
|
142
|
+
onRowClick={onRowClick}
|
|
143
|
+
onRowDoubleClick={onRowDoubleClick}
|
|
144
|
+
allowChildRows
|
|
145
|
+
// onSelectionChange={onSelectionChange}
|
|
146
|
+
// selected={selected}
|
|
147
|
+
contextMenuItems={contextMenuItems as any}
|
|
148
|
+
customColumns={customColumns}
|
|
149
|
+
columns={columns}
|
|
150
|
+
editableColumns={editableColumns}
|
|
151
|
+
editable={true}
|
|
152
|
+
onEditComplete={onEditComplete}
|
|
153
|
+
advancedFilterFields={['name']}
|
|
154
|
+
advancedFilterLabel="Search Name"
|
|
155
|
+
advancedFilterPlaceholder={<i>Type a name</i>}
|
|
156
|
+
// leftColumns={leftColumns}
|
|
157
|
+
gridOptions={
|
|
158
|
+
{
|
|
159
|
+
defaultFilterValue: [{ name: 'name', operator: 'contains', type: 'string', value: '' }],
|
|
160
|
+
pagination: 'local',
|
|
161
|
+
defaultLimit: 10,
|
|
162
|
+
pageSizes: [10, 25, 50],
|
|
163
|
+
checkboxColumn: true,
|
|
164
|
+
multiSelect: true,
|
|
165
|
+
// consumer onRenderRow runs after the internal one
|
|
166
|
+
onRenderRow: (rowProps: any) => {
|
|
167
|
+
rowProps['data-example-row'] = 'true';
|
|
168
|
+
},
|
|
169
|
+
// defaultFilterValue: filterValue,
|
|
170
|
+
// remoteFilter: false,
|
|
171
|
+
// multiSelect: true,
|
|
172
|
+
// pagination: 'local',
|
|
173
|
+
// defaultLimit: 100,
|
|
174
|
+
// limit: 100,
|
|
175
|
+
// pageSizes: [100],
|
|
176
|
+
// rowIndexColumn: true,
|
|
177
|
+
// checkboxColumn: true,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/>
|
|
181
|
+
</section>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
export default ExampleGrid;
|
|
@@ -1,157 +1,158 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { DataGridWrapperProps, GridType } from '../types/types';
|
|
3
|
-
import { find_realData, get_row_id } from '../func/func_grid';
|
|
4
|
-
import { func_selection } from '../func/func_child';
|
|
5
|
-
import {
|
|
6
|
-
apply_root_a11y,
|
|
7
|
-
apply_row_a11y,
|
|
8
|
-
get_grid_root,
|
|
9
|
-
get_root_a11y_key,
|
|
10
|
-
is_grid_busy,
|
|
11
|
-
} from '../func/func_a11y';
|
|
12
|
-
|
|
13
|
-
function useGridMenuRow() {
|
|
14
|
-
const [menuAnchorEl, set_menuAnchorEl] = React.useState<any>(null);
|
|
15
|
-
const [menuRow, set_menuRow] = React.useState<any>(null);
|
|
16
|
-
const update_menuRow = React.useCallback((r: any = null, el: any = null) => {
|
|
17
|
-
set_menuRow(r || null);
|
|
18
|
-
set_menuAnchorEl(el || null);
|
|
19
|
-
}, []);
|
|
20
|
-
return { update_menuRow, menuAnchorEl, menuRow };
|
|
21
|
-
}
|
|
22
|
-
export function useGridEvents(grid: GridType, props: DataGridWrapperProps) {
|
|
23
|
-
const click_data = React.useRef<any>(null);
|
|
24
|
-
// latest props, so the callbacks below stay stable and don't re-render the grid
|
|
25
|
-
const latestProps = React.useRef(props);
|
|
26
|
-
latestProps.current = props;
|
|
27
|
-
const { update_menuRow, menuAnchorEl, menuRow } = useGridMenuRow();
|
|
28
|
-
|
|
29
|
-
const refreshColumn = React.useCallback(
|
|
30
|
-
(col: string) => {
|
|
31
|
-
const dt = grid.ungroupedData;
|
|
32
|
-
for (let index = 0; index < dt.length; index++) {
|
|
33
|
-
(grid as any).setItemPropertyAt(index, col, dt[index][col]);
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
[grid]
|
|
37
|
-
);
|
|
38
|
-
const refresh = React.useCallback(() => {
|
|
39
|
-
const dt = grid.ungroupedData;
|
|
40
|
-
for (let index = 0; index < dt.length; index++) {
|
|
41
|
-
(grid as any).setItemAt(index, {}, { replace: false });
|
|
42
|
-
}
|
|
43
|
-
}, [grid]);
|
|
44
|
-
|
|
45
|
-
React.useEffect(() => {
|
|
46
|
-
if (!grid) return;
|
|
47
|
-
grid['refresh'] = refresh;
|
|
48
|
-
grid['refreshColumn'] = refreshColumn;
|
|
49
|
-
}, [grid, props.dataSource, refresh, refreshColumn]);
|
|
50
|
-
/* -------------------------------------------------------------------------- */
|
|
51
|
-
/* events */
|
|
52
|
-
/* -------------------------------------------------------------------------- */
|
|
53
|
-
const onRowClick = React.useCallback((rowProps, event) => {
|
|
54
|
-
const { onRowClick, onRowDoubleClick } = latestProps.current;
|
|
55
|
-
const nowDate = Date.now();
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
key
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
rootSyncFrame.current
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DataGridWrapperProps, GridType } from '../types/types';
|
|
3
|
+
import { find_realData, get_row_id } from '../func/func_grid';
|
|
4
|
+
import { func_selection } from '../func/func_child';
|
|
5
|
+
import {
|
|
6
|
+
apply_root_a11y,
|
|
7
|
+
apply_row_a11y,
|
|
8
|
+
get_grid_root,
|
|
9
|
+
get_root_a11y_key,
|
|
10
|
+
is_grid_busy,
|
|
11
|
+
} from '../func/func_a11y';
|
|
12
|
+
|
|
13
|
+
function useGridMenuRow() {
|
|
14
|
+
const [menuAnchorEl, set_menuAnchorEl] = React.useState<any>(null);
|
|
15
|
+
const [menuRow, set_menuRow] = React.useState<any>(null);
|
|
16
|
+
const update_menuRow = React.useCallback((r: any = null, el: any = null) => {
|
|
17
|
+
set_menuRow(r || null);
|
|
18
|
+
set_menuAnchorEl(el || null);
|
|
19
|
+
}, []);
|
|
20
|
+
return { update_menuRow, menuAnchorEl, menuRow };
|
|
21
|
+
}
|
|
22
|
+
export function useGridEvents(grid: GridType, props: DataGridWrapperProps) {
|
|
23
|
+
const click_data = React.useRef<any>(null);
|
|
24
|
+
// latest props, so the callbacks below stay stable and don't re-render the grid
|
|
25
|
+
const latestProps = React.useRef(props);
|
|
26
|
+
latestProps.current = props;
|
|
27
|
+
const { update_menuRow, menuAnchorEl, menuRow } = useGridMenuRow();
|
|
28
|
+
|
|
29
|
+
const refreshColumn = React.useCallback(
|
|
30
|
+
(col: string) => {
|
|
31
|
+
const dt = grid.ungroupedData;
|
|
32
|
+
for (let index = 0; index < dt.length; index++) {
|
|
33
|
+
(grid as any).setItemPropertyAt(index, col, dt[index][col]);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[grid]
|
|
37
|
+
);
|
|
38
|
+
const refresh = React.useCallback(() => {
|
|
39
|
+
const dt = grid.ungroupedData;
|
|
40
|
+
for (let index = 0; index < dt.length; index++) {
|
|
41
|
+
(grid as any).setItemAt(index, {}, { replace: false });
|
|
42
|
+
}
|
|
43
|
+
}, [grid]);
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (!grid) return;
|
|
47
|
+
grid['refresh'] = refresh;
|
|
48
|
+
grid['refreshColumn'] = refreshColumn;
|
|
49
|
+
}, [grid, props.dataSource, refresh, refreshColumn]);
|
|
50
|
+
/* -------------------------------------------------------------------------- */
|
|
51
|
+
/* events */
|
|
52
|
+
/* -------------------------------------------------------------------------- */
|
|
53
|
+
const onRowClick = React.useCallback((rowProps, event) => {
|
|
54
|
+
const { onRowClick, onRowDoubleClick } = latestProps.current;
|
|
55
|
+
const nowDate = Date.now();
|
|
56
|
+
const rowId = get_row_id(rowProps.data);
|
|
57
|
+
const lastclick: number = click_data.current[rowId];
|
|
58
|
+
if (lastclick) {
|
|
59
|
+
const diffTime = Math.abs(nowDate - lastclick);
|
|
60
|
+
if (diffTime < 400) {
|
|
61
|
+
click_data.current[rowId] = null;
|
|
62
|
+
return onRowDoubleClick && onRowDoubleClick(rowProps.rowIndex, rowProps.data);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
click_data.current[rowId] = nowDate;
|
|
67
|
+
return onRowClick && onRowClick(rowProps.rowIndex, rowProps.data);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
click_data.current = {};
|
|
72
|
+
}, [props.dataSource]);
|
|
73
|
+
|
|
74
|
+
/* ------------------------------- ContextMenu ------------------------------ */
|
|
75
|
+
const onRowContextMenu = React.useCallback((rowProps, event) => {
|
|
76
|
+
if (!rowProps || !rowProps.data || func_selection.has_selection()) {
|
|
77
|
+
update_menuRow(null, null);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
|
|
82
|
+
const realRow = find_realData(rowProps.dataSourceArray, event.target, rowProps.data);
|
|
83
|
+
const anEl = func_selection.getVirtualElement(event.clientX, event.clientY);
|
|
84
|
+
update_menuRow(realRow, anEl);
|
|
85
|
+
return false;
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
/* -------------------------------------------------------------------------- */
|
|
89
|
+
/* ------------------------------ Accessibility ----------------------------- */
|
|
90
|
+
const rootSyncFrame = React.useRef(0);
|
|
91
|
+
// last applied root values, so row renders (e.g. while scrolling) don't re-apply them
|
|
92
|
+
const lastRoot = React.useRef<{ el: Element | null; key: string; busy: boolean }>({
|
|
93
|
+
el: null,
|
|
94
|
+
key: '',
|
|
95
|
+
busy: false,
|
|
96
|
+
});
|
|
97
|
+
const getRootOptions = () => ({
|
|
98
|
+
ariaLabel: latestProps.current.ariaLabel,
|
|
99
|
+
editable: !!latestProps.current.editable,
|
|
100
|
+
});
|
|
101
|
+
// root attributes are set on the DOM node, batched to once per frame
|
|
102
|
+
const scheduleRootSync = React.useCallback((force = false) => {
|
|
103
|
+
if (rootSyncFrame.current || typeof requestAnimationFrame === 'undefined') return;
|
|
104
|
+
const grid = latestProps.current.liveGridRef?.current?.current;
|
|
105
|
+
const key = get_root_a11y_key(grid, getRootOptions());
|
|
106
|
+
if (
|
|
107
|
+
!force &&
|
|
108
|
+
key &&
|
|
109
|
+
key === lastRoot.current.key &&
|
|
110
|
+
get_grid_root(grid) === lastRoot.current.el
|
|
111
|
+
)
|
|
112
|
+
return;
|
|
113
|
+
rootSyncFrame.current = requestAnimationFrame(() => {
|
|
114
|
+
rootSyncFrame.current = 0;
|
|
115
|
+
const liveGrid = latestProps.current.liveGridRef?.current?.current;
|
|
116
|
+
const options = getRootOptions();
|
|
117
|
+
const el = apply_root_a11y(liveGrid, options);
|
|
118
|
+
lastRoot.current = {
|
|
119
|
+
el,
|
|
120
|
+
key: el ? get_root_a11y_key(liveGrid, options) : '',
|
|
121
|
+
busy: el ? is_grid_busy(liveGrid) : false,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}, []);
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
scheduleRootSync();
|
|
127
|
+
}, [scheduleRootSync, props.ariaLabel, props.editable, props.columns, props.dataSource]);
|
|
128
|
+
React.useEffect(
|
|
129
|
+
() => () => {
|
|
130
|
+
if (rootSyncFrame.current) cancelAnimationFrame(rootSyncFrame.current);
|
|
131
|
+
rootSyncFrame.current = 0;
|
|
132
|
+
},
|
|
133
|
+
[]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const onRenderRow = React.useCallback((rowProps) => {
|
|
137
|
+
// save the original handlers to be called later
|
|
138
|
+
rowProps['data-row-id'] = get_row_id(rowProps.data);
|
|
139
|
+
const liveGrid: any = latestProps.current.liveGridRef?.current?.current;
|
|
140
|
+
apply_row_a11y(rowProps, !!liveGrid?.computedRowSelectionEnabled);
|
|
141
|
+
scheduleRootSync();
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
// called by the grid on every render with the load mask state, keeps aria-busy in sync
|
|
145
|
+
const onRenderLoadMask = React.useCallback((maskProps: { visible?: boolean }) => {
|
|
146
|
+
if (!!maskProps?.visible !== lastRoot.current.busy) scheduleRootSync(true);
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
onRowClick,
|
|
151
|
+
onRenderLoadMask,
|
|
152
|
+
onRowContextMenu,
|
|
153
|
+
onRenderRow,
|
|
154
|
+
update_menuRow,
|
|
155
|
+
menuAnchorEl,
|
|
156
|
+
menuRow,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -1,106 +1,146 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { DataGridWrapperProps } from '../types/types';
|
|
3
|
-
import debounce from 'lodash.debounce';
|
|
4
|
-
import { array_intersection, fn_eval } from '@azrico/object';
|
|
5
|
-
|
|
6
|
-
const SEARCH_DELAY = 300;
|
|
7
|
-
|
|
8
|
-
function to_rows(result: any): any[] {
|
|
9
|
-
return result && typeof result === 'object' ? result.data || result : result;
|
|
10
|
-
}
|
|
11
|
-
//false when the result is only a page of the data (remote pagination)
|
|
12
|
-
function is_complete(result: any) {
|
|
13
|
-
if (!result || typeof result !== 'object' || Array.isArray(result)) return true;
|
|
14
|
-
const loaded = Array.isArray(result.data) ? result.data.length : 0;
|
|
15
|
-
return !(result.count > loaded);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function useGridSearch(props: DataGridWrapperProps) {
|
|
19
|
-
const [search, set_search] = React.useState('');
|
|
20
|
-
// null shows the datasource itself, an array shows search results
|
|
21
|
-
const [searchResult, set_searchResult] = React.useState<any[] | null>(null);
|
|
22
|
-
const latestProps = React.useRef(props);
|
|
23
|
-
latestProps.current = props;
|
|
24
|
-
// last result the grid loaded, reused by search instead of loading the data again
|
|
25
|
-
const loaded = React.useRef<{ ds: any; result: any } | null>(null);
|
|
26
|
-
const searchId = React.useRef(0);
|
|
27
|
-
|
|
28
|
-
const trackedDataSource = React.useCallback(
|
|
29
|
-
async (args: any) => {
|
|
30
|
-
const result = await fn_eval(props.dataSource, args);
|
|
31
|
-
loaded.current = { ds: props.dataSource, result };
|
|
32
|
-
return result;
|
|
33
|
-
},
|
|
34
|
-
[props.dataSource]
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const get_search_rows = React.useCallback(async () => {
|
|
38
|
-
const ds = latestProps.current.dataSource;
|
|
39
|
-
const last = loaded.current;
|
|
40
|
-
if (last && last.ds === ds && is_complete(last.result)) return to_rows(last.result);
|
|
41
|
-
return to_rows(await fn_eval(ds));
|
|
42
|
-
}, []);
|
|
43
|
-
|
|
44
|
-
const filter_rows = React.useCallback((rows: any[], text: string) => {
|
|
45
|
-
const { gridRef, advancedFilterFields } = latestProps.current;
|
|
46
|
-
const visibleColumns = gridRef.current?.visibleColumns;
|
|
47
|
-
const columns_to_search: string[] = visibleColumns
|
|
48
|
-
? array_intersection(
|
|
49
|
-
advancedFilterFields || [],
|
|
50
|
-
visibleColumns.map((r) => r.id)
|
|
51
|
-
)
|
|
52
|
-
: advancedFilterFields || [];
|
|
53
|
-
const lowerSearchText = text.toLowerCase().trim();
|
|
54
|
-
if (!lowerSearchText || columns_to_search.length === 0) return rows;
|
|
55
|
-
return rows.filter((p) => {
|
|
56
|
-
for (let index = 0; index < columns_to_search.length; index++) {
|
|
57
|
-
const col = columns_to_search[index];
|
|
58
|
-
const v = (p[col] + '').toLowerCase(); //value of column
|
|
59
|
-
if (v && v.indexOf(lowerSearchText) != -1) {
|
|
60
|
-
//if one column is matching then the row is a match
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return false;
|
|
65
|
-
});
|
|
66
|
-
}, []);
|
|
67
|
-
|
|
68
|
-
const run_search = React.useMemo(
|
|
69
|
-
() =>
|
|
70
|
-
debounce(async (text: string) => {
|
|
71
|
-
const id = ++searchId.current;
|
|
72
|
-
const rows = await get_search_rows();
|
|
73
|
-
if (id !== searchId.current || !Array.isArray(rows)) return; //a newer search started
|
|
74
|
-
set_searchResult(filter_rows(rows, text));
|
|
75
|
-
}, SEARCH_DELAY),
|
|
76
|
-
[]
|
|
77
|
-
);
|
|
78
|
-
React.useEffect(() => () => run_search.cancel(), [run_search]);
|
|
79
|
-
|
|
80
|
-
React.useEffect(() => {
|
|
81
|
-
if (search) {
|
|
82
|
-
run_search(search);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
run_search.cancel();
|
|
86
|
-
searchId.current++;
|
|
87
|
-
set_searchResult(null);
|
|
88
|
-
}, [search, props.dataSource]);
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DataGridWrapperProps } from '../types/types';
|
|
3
|
+
import debounce from 'lodash.debounce';
|
|
4
|
+
import { array_intersection, fn_eval } from '@azrico/object';
|
|
5
|
+
|
|
6
|
+
const SEARCH_DELAY = 300;
|
|
7
|
+
|
|
8
|
+
function to_rows(result: any): any[] {
|
|
9
|
+
return result && typeof result === 'object' ? result.data || result : result;
|
|
10
|
+
}
|
|
11
|
+
//false when the result is only a page of the data (remote pagination)
|
|
12
|
+
function is_complete(result: any) {
|
|
13
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) return true;
|
|
14
|
+
const loaded = Array.isArray(result.data) ? result.data.length : 0;
|
|
15
|
+
return !(result.count > loaded);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useGridSearch(props: DataGridWrapperProps) {
|
|
19
|
+
const [search, set_search] = React.useState('');
|
|
20
|
+
// null shows the datasource itself, an array shows search results
|
|
21
|
+
const [searchResult, set_searchResult] = React.useState<any[] | null>(null);
|
|
22
|
+
const latestProps = React.useRef(props);
|
|
23
|
+
latestProps.current = props;
|
|
24
|
+
// last result the grid loaded, reused by search instead of loading the data again
|
|
25
|
+
const loaded = React.useRef<{ ds: any; result: any } | null>(null);
|
|
26
|
+
const searchId = React.useRef(0);
|
|
27
|
+
|
|
28
|
+
const trackedDataSource = React.useCallback(
|
|
29
|
+
async (args: any) => {
|
|
30
|
+
const result = await fn_eval(props.dataSource, args);
|
|
31
|
+
loaded.current = { ds: props.dataSource, result };
|
|
32
|
+
return result;
|
|
33
|
+
},
|
|
34
|
+
[props.dataSource]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const get_search_rows = React.useCallback(async () => {
|
|
38
|
+
const ds = latestProps.current.dataSource;
|
|
39
|
+
const last = loaded.current;
|
|
40
|
+
if (last && last.ds === ds && is_complete(last.result)) return to_rows(last.result);
|
|
41
|
+
return to_rows(await fn_eval(ds));
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const filter_rows = React.useCallback((rows: any[], text: string) => {
|
|
45
|
+
const { gridRef, advancedFilterFields } = latestProps.current;
|
|
46
|
+
const visibleColumns = gridRef.current?.visibleColumns;
|
|
47
|
+
const columns_to_search: string[] = visibleColumns
|
|
48
|
+
? array_intersection(
|
|
49
|
+
advancedFilterFields || [],
|
|
50
|
+
visibleColumns.map((r) => r.id)
|
|
51
|
+
)
|
|
52
|
+
: advancedFilterFields || [];
|
|
53
|
+
const lowerSearchText = text.toLowerCase().trim();
|
|
54
|
+
if (!lowerSearchText || columns_to_search.length === 0) return rows;
|
|
55
|
+
return rows.filter((p) => {
|
|
56
|
+
for (let index = 0; index < columns_to_search.length; index++) {
|
|
57
|
+
const col = columns_to_search[index];
|
|
58
|
+
const v = (p[col] + '').toLowerCase(); //value of column
|
|
59
|
+
if (v && v.indexOf(lowerSearchText) != -1) {
|
|
60
|
+
//if one column is matching then the row is a match
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
});
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const run_search = React.useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
debounce(async (text: string) => {
|
|
71
|
+
const id = ++searchId.current;
|
|
72
|
+
const rows = await get_search_rows();
|
|
73
|
+
if (id !== searchId.current || !Array.isArray(rows)) return; //a newer search started
|
|
74
|
+
set_searchResult(filter_rows(rows, text));
|
|
75
|
+
}, SEARCH_DELAY),
|
|
76
|
+
[]
|
|
77
|
+
);
|
|
78
|
+
React.useEffect(() => () => run_search.cancel(), [run_search]);
|
|
79
|
+
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
if (search) {
|
|
82
|
+
run_search(search);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
run_search.cancel();
|
|
86
|
+
searchId.current++;
|
|
87
|
+
set_searchResult(null);
|
|
88
|
+
}, [search, props.dataSource]);
|
|
89
|
+
|
|
90
|
+
const { advancedFilterLabel, advancedFilterPlaceholder } = props;
|
|
91
|
+
const textPlaceholder =
|
|
92
|
+
typeof advancedFilterPlaceholder === 'string' || typeof advancedFilterPlaceholder === 'number'
|
|
93
|
+
? String(advancedFilterPlaceholder)
|
|
94
|
+
: undefined;
|
|
95
|
+
// custom HTML cannot go in the native placeholder attribute, so it is shown over the empty input
|
|
96
|
+
const nodePlaceholder =
|
|
97
|
+
textPlaceholder === undefined &&
|
|
98
|
+
advancedFilterPlaceholder !== undefined &&
|
|
99
|
+
advancedFilterPlaceholder !== null &&
|
|
100
|
+
typeof advancedFilterPlaceholder !== 'boolean'
|
|
101
|
+
? advancedFilterPlaceholder
|
|
102
|
+
: undefined;
|
|
103
|
+
const input = (
|
|
104
|
+
<input
|
|
105
|
+
type="text"
|
|
106
|
+
name="name"
|
|
107
|
+
value={search}
|
|
108
|
+
placeholder={textPlaceholder}
|
|
109
|
+
onChange={(e) => set_search(e.target.value)}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const element = (
|
|
114
|
+
<>
|
|
115
|
+
<form className={'azgrid-searcharea'}>
|
|
116
|
+
<label>
|
|
117
|
+
{advancedFilterLabel ?? 'Search'}
|
|
118
|
+
{nodePlaceholder === undefined ? (
|
|
119
|
+
input
|
|
120
|
+
) : (
|
|
121
|
+
<span style={{ position: 'relative', display: 'inline-block' }}>
|
|
122
|
+
{input}
|
|
123
|
+
{search ? null : (
|
|
124
|
+
<span
|
|
125
|
+
style={{
|
|
126
|
+
position: 'absolute',
|
|
127
|
+
left: 8,
|
|
128
|
+
top: 0,
|
|
129
|
+
bottom: 0,
|
|
130
|
+
display: 'flex',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
pointerEvents: 'none',
|
|
133
|
+
opacity: 0.6,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{nodePlaceholder}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
</label>
|
|
142
|
+
</form>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
return { filteredData: searchResult ?? trackedDataSource, element };
|
|
146
|
+
}
|
package/src/types/types.ts
CHANGED
|
@@ -116,6 +116,10 @@ export type AzGridProps = {
|
|
|
116
116
|
//
|
|
117
117
|
editable?: boolean | ((editValue: any, cellProps: any) => boolean);
|
|
118
118
|
advancedFilterFields?: string[];
|
|
119
|
+
/** Label of the quick-search box shown with `advancedFilterFields`. Default "Search". */
|
|
120
|
+
advancedFilterLabel?: React.ReactNode;
|
|
121
|
+
/** Placeholder of the quick-search input. A string is used as the native placeholder, any other node (custom HTML) is shown over the empty input. */
|
|
122
|
+
advancedFilterPlaceholder?: React.ReactNode;
|
|
119
123
|
//
|
|
120
124
|
|
|
121
125
|
loading?: boolean;
|