@etsoo/materialui 1.2.1 → 1.2.3
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/lib/ComboBoxPro.js +5 -2
- package/lib/SearchBar.js +1 -1
- package/lib/TiplistPro.d.ts +47 -0
- package/lib/TiplistPro.js +173 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +10 -10
- package/src/ComboBoxPro.tsx +5 -4
- package/src/SearchBar.tsx +1 -1
- package/src/Tiplist.tsx +0 -1
- package/src/TiplistPro.tsx +339 -0
- package/src/index.ts +1 -0
package/lib/ComboBoxPro.js
CHANGED
|
@@ -6,7 +6,6 @@ export function ComboBoxPro(props) {
|
|
|
6
6
|
var _a;
|
|
7
7
|
// Labels
|
|
8
8
|
const { noOptions, loading: loadingLabel, open: openDefault } = (_a = globalApp === null || globalApp === void 0 ? void 0 : globalApp.getLabels("noOptions", "loading", "open")) !== null && _a !== void 0 ? _a : {};
|
|
9
|
-
const getLabel = (item) => "label" in item ? item.label : "name" in item ? item.name : "";
|
|
10
9
|
// Destruct
|
|
11
10
|
const { noOptionsText = noOptions, loadingText = loadingLabel, openText = openDefault, options, openOnFocus = true, label, inputProps, name, value, idValue, onChange, ...rest } = props;
|
|
12
11
|
const [open, setOpen] = React.useState(false);
|
|
@@ -47,7 +46,11 @@ export function ComboBoxPro(props) {
|
|
|
47
46
|
}, options: localOptions, loading: loading, openOnFocus: openOnFocus, renderInput: (params) => (React.createElement(InputField, { ...inputProps, ...params, label: label, name: name, onBlur: (event) => {
|
|
48
47
|
if (localValue == null && onChange)
|
|
49
48
|
onChange(event, event.target.value, "blur", undefined);
|
|
50
|
-
} })), getOptionLabel: (item) => typeof item === "object"
|
|
49
|
+
} })), getOptionLabel: (item) => typeof item === "object"
|
|
50
|
+
? "label" in item
|
|
51
|
+
? item.label
|
|
52
|
+
: item.name
|
|
53
|
+
: item, isOptionEqualToValue: (option, value) => option.id === value.id, noOptionsText: noOptionsText, loadingText: loadingText, openText: openText, onChange: (event, value, reason, details) => {
|
|
51
54
|
setValue(value);
|
|
52
55
|
if (onChange)
|
|
53
56
|
onChange(event, value, reason, details);
|
package/lib/SearchBar.js
CHANGED
|
@@ -244,7 +244,7 @@ export function SearchBar(props) {
|
|
|
244
244
|
React.createElement(IconButton, { title: labels.more, size: "medium", sx: { height: "40px" }, onClick: handleMore },
|
|
245
245
|
React.createElement(MoreHorizIcon, null)),
|
|
246
246
|
React.createElement(Button, { variant: "contained", size: "medium", ref: resetButtonRef, onClick: handleReset }, labels.reset))),
|
|
247
|
-
hasMoreItems && (React.createElement(Drawer, { anchor: "right", sx: { minWidth: "
|
|
247
|
+
hasMoreItems && (React.createElement(Drawer, { anchor: "right", sx: { minWidth: "180px" }, ModalProps: {
|
|
248
248
|
keepMounted: true
|
|
249
249
|
}, open: open, onClose: () => updateOpen(false) },
|
|
250
250
|
React.createElement("form", { onChange: moreFormChange, ref: (form) => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ListType2 } from "@etsoo/shared";
|
|
2
|
+
import { AutocompleteProps } from "@mui/material";
|
|
3
|
+
import { ChangeEventHandler } from "react";
|
|
4
|
+
import { InputFieldProps } from "./InputField";
|
|
5
|
+
/**
|
|
6
|
+
* TiplistPro props
|
|
7
|
+
*/
|
|
8
|
+
export type TiplistProProps<T extends ListType2 = ListType2> = Omit<AutocompleteProps<T, false, false, true>, "open" | "multiple" | "options" | "renderInput"> & {
|
|
9
|
+
/**
|
|
10
|
+
* Load data callback
|
|
11
|
+
*/
|
|
12
|
+
loadData: (keyword: string | undefined, id: T["id"] | undefined, maxItems: number) => PromiseLike<T[] | null | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Max items to read and display
|
|
15
|
+
*/
|
|
16
|
+
maxItems?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Width
|
|
19
|
+
*/
|
|
20
|
+
width?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Label
|
|
23
|
+
*/
|
|
24
|
+
label?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Field name
|
|
27
|
+
*/
|
|
28
|
+
name?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Id value
|
|
31
|
+
*/
|
|
32
|
+
idValue?: T["id"] | null;
|
|
33
|
+
/**
|
|
34
|
+
* Input onChange hanlder
|
|
35
|
+
*/
|
|
36
|
+
inputOnChange?: ChangeEventHandler<HTMLInputElement> | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Input props
|
|
39
|
+
*/
|
|
40
|
+
inputProps?: Omit<InputFieldProps, "onChange">;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* TiplistPro
|
|
44
|
+
* @param props Props
|
|
45
|
+
* @returns Component
|
|
46
|
+
*/
|
|
47
|
+
export declare function TiplistPro<T extends ListType2 = ListType2>(props: TiplistProProps<T>): JSX.Element;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { ReactUtils, useDelayedExecutor } from "@etsoo/react";
|
|
2
|
+
import { Autocomplete } from "@mui/material";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { InputField } from "./InputField";
|
|
5
|
+
import { globalApp } from "./app/ReactApp";
|
|
6
|
+
/**
|
|
7
|
+
* TiplistPro
|
|
8
|
+
* @param props Props
|
|
9
|
+
* @returns Component
|
|
10
|
+
*/
|
|
11
|
+
export function TiplistPro(props) {
|
|
12
|
+
var _a;
|
|
13
|
+
// Labels
|
|
14
|
+
const { noOptions, loading, more, open: openDefault } = (_a = globalApp === null || globalApp === void 0 ? void 0 : globalApp.getLabels("noOptions", "loading", "more", "open")) !== null && _a !== void 0 ? _a : {};
|
|
15
|
+
// Destruct
|
|
16
|
+
const { label, loadData, defaultValue, value, idValue, maxItems = 16, width, name, inputOnChange, inputProps, sx, openOnFocus = true, noOptionsText = noOptions, loadingText = loading, openText = openDefault, getOptionDisabled, getOptionLabel, onChange, ...rest } = props;
|
|
17
|
+
if (width && sx)
|
|
18
|
+
Object.assign(sx, { width: `${width}px` });
|
|
19
|
+
// Value input ref
|
|
20
|
+
const inputRef = React.createRef();
|
|
21
|
+
// Local value
|
|
22
|
+
let localValue = value !== null && value !== void 0 ? value : defaultValue;
|
|
23
|
+
// One time calculation for input's default value (uncontrolled)
|
|
24
|
+
const localIdValue = idValue !== null && idValue !== void 0 ? idValue : (localValue != null && typeof localValue === "object"
|
|
25
|
+
? localValue.id
|
|
26
|
+
: null);
|
|
27
|
+
// Changable states
|
|
28
|
+
const [states, stateUpdate] = React.useReducer((currentState, newState) => {
|
|
29
|
+
return { ...currentState, ...newState };
|
|
30
|
+
}, {
|
|
31
|
+
// Loading unknown
|
|
32
|
+
open: false,
|
|
33
|
+
options: [],
|
|
34
|
+
value: null
|
|
35
|
+
});
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
if (localValue != value)
|
|
38
|
+
stateUpdate({ value: localValue });
|
|
39
|
+
}, [localValue]);
|
|
40
|
+
// Input value
|
|
41
|
+
const inputValue = React.useMemo(() => states.value && typeof states.value === "object"
|
|
42
|
+
? states.value.id
|
|
43
|
+
: undefined, [states.value]);
|
|
44
|
+
// State
|
|
45
|
+
const [state] = React.useState({});
|
|
46
|
+
const isMounted = React.useRef(true);
|
|
47
|
+
// Change handler
|
|
48
|
+
const changeHandle = (event) => {
|
|
49
|
+
// Stop processing with auto trigger event
|
|
50
|
+
if (event.nativeEvent.cancelable && !event.nativeEvent.composed) {
|
|
51
|
+
stateUpdate({ options: [] });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Stop bubble
|
|
55
|
+
event.stopPropagation();
|
|
56
|
+
// Call with delay
|
|
57
|
+
delayed.call(undefined, event.currentTarget.value);
|
|
58
|
+
};
|
|
59
|
+
// Directly load data
|
|
60
|
+
const loadDataDirect = (keyword, id) => {
|
|
61
|
+
// Reset options
|
|
62
|
+
// setOptions([]);
|
|
63
|
+
if (id == null) {
|
|
64
|
+
// Reset real value
|
|
65
|
+
const input = inputRef.current;
|
|
66
|
+
if (input && input.value !== "") {
|
|
67
|
+
// Different value, trigger change event
|
|
68
|
+
ReactUtils.triggerChange(input, "", false);
|
|
69
|
+
}
|
|
70
|
+
if (states.options.length > 0) {
|
|
71
|
+
// Reset options
|
|
72
|
+
stateUpdate({ options: [] });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Loading indicator
|
|
76
|
+
if (!states.loading)
|
|
77
|
+
stateUpdate({ loading: true });
|
|
78
|
+
// Load list
|
|
79
|
+
loadData(keyword, id, maxItems).then((options) => {
|
|
80
|
+
if (!isMounted.current)
|
|
81
|
+
return;
|
|
82
|
+
if (options != null && options.length >= maxItems) {
|
|
83
|
+
options.push({ id: -1, name: "n/a" });
|
|
84
|
+
}
|
|
85
|
+
// Indicates loading completed
|
|
86
|
+
stateUpdate({
|
|
87
|
+
loading: false,
|
|
88
|
+
...(options != null && { options })
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
const delayed = useDelayedExecutor(loadDataDirect, 480);
|
|
93
|
+
const setInputValue = (value) => {
|
|
94
|
+
var _a;
|
|
95
|
+
stateUpdate({ value });
|
|
96
|
+
// Input value
|
|
97
|
+
const input = inputRef.current;
|
|
98
|
+
if (input) {
|
|
99
|
+
// Update value
|
|
100
|
+
const newValue = (_a = value === null || value === void 0 ? void 0 : value.id.toString()) !== null && _a !== void 0 ? _a : "";
|
|
101
|
+
if (newValue !== input.value) {
|
|
102
|
+
// Different value, trigger change event
|
|
103
|
+
ReactUtils.triggerChange(input, newValue, false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
if (localIdValue != null && localIdValue !== "") {
|
|
108
|
+
if (state.idLoaded) {
|
|
109
|
+
// Set default
|
|
110
|
+
if (!state.idSet && states.options.length == 1) {
|
|
111
|
+
stateUpdate({ value: states.options[0] });
|
|
112
|
+
state.idSet = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Load id data
|
|
117
|
+
loadDataDirect(undefined, localIdValue);
|
|
118
|
+
state.idLoaded = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
React.useEffect(() => {
|
|
122
|
+
return () => {
|
|
123
|
+
isMounted.current = false;
|
|
124
|
+
delayed.clear();
|
|
125
|
+
};
|
|
126
|
+
}, []);
|
|
127
|
+
// Layout
|
|
128
|
+
return (React.createElement("div", null,
|
|
129
|
+
React.createElement("input", { ref: inputRef, "data-reset": "true", type: "text", style: { display: "none" }, name: name, value: inputValue !== null && inputValue !== void 0 ? inputValue : "", readOnly: true, onChange: inputOnChange }),
|
|
130
|
+
React.createElement(Autocomplete, { filterOptions: (options, _state) => options, value: states.value, options: states.options, freeSolo: true, clearOnBlur: false, onChange: (event, value, reason, details) => {
|
|
131
|
+
if (typeof value === "object") {
|
|
132
|
+
// Set value
|
|
133
|
+
setInputValue(value);
|
|
134
|
+
}
|
|
135
|
+
// Custom
|
|
136
|
+
if (onChange != null)
|
|
137
|
+
onChange(event, value, reason, details);
|
|
138
|
+
// For clear case
|
|
139
|
+
if (reason === "clear") {
|
|
140
|
+
stateUpdate({ options: [] });
|
|
141
|
+
loadDataDirect();
|
|
142
|
+
}
|
|
143
|
+
}, open: states.open, openOnFocus: openOnFocus, onOpen: () => {
|
|
144
|
+
// Should load
|
|
145
|
+
const loading = states.loading ? true : states.options.length === 0;
|
|
146
|
+
stateUpdate({ open: true, loading });
|
|
147
|
+
// If not loading
|
|
148
|
+
if (loading)
|
|
149
|
+
loadDataDirect(undefined, states.value && typeof states.value === "object"
|
|
150
|
+
? states.value.id
|
|
151
|
+
: undefined);
|
|
152
|
+
}, onClose: () => {
|
|
153
|
+
stateUpdate({
|
|
154
|
+
open: false,
|
|
155
|
+
...(!states.value && { options: [] })
|
|
156
|
+
});
|
|
157
|
+
}, loading: states.loading, renderInput: (params) => (React.createElement(InputField, { ...inputProps, ...params, onChange: changeHandle, label: label, name: name + "Input", onBlur: (event) => {
|
|
158
|
+
if (states.value == null && onChange)
|
|
159
|
+
onChange(event, event.target.value, "blur", undefined);
|
|
160
|
+
} })), isOptionEqualToValue: (option, value) => option.id === value.id, sx: sx, noOptionsText: noOptionsText, loadingText: loadingText, openText: openText, getOptionDisabled: (item) => {
|
|
161
|
+
if (item.id === -1)
|
|
162
|
+
return true;
|
|
163
|
+
return getOptionDisabled ? getOptionDisabled(item) : false;
|
|
164
|
+
}, getOptionLabel: (item) => {
|
|
165
|
+
if (typeof item === "string")
|
|
166
|
+
return item;
|
|
167
|
+
if (item["id"] === -1)
|
|
168
|
+
return (more !== null && more !== void 0 ? more : "More") + "...";
|
|
169
|
+
if (getOptionLabel == null)
|
|
170
|
+
return "label" in item ? item.label : item.name;
|
|
171
|
+
return getOptionLabel(item);
|
|
172
|
+
}, ...rest })));
|
|
173
|
+
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/materialui",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "TypeScript Material-UI Implementation",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"@emotion/css": "^11.10.6",
|
|
51
51
|
"@emotion/react": "^11.10.6",
|
|
52
52
|
"@emotion/styled": "^11.10.6",
|
|
53
|
-
"@etsoo/appscript": "^1.3.
|
|
53
|
+
"@etsoo/appscript": "^1.3.92",
|
|
54
54
|
"@etsoo/notificationbase": "^1.1.24",
|
|
55
|
-
"@etsoo/react": "^1.6.
|
|
56
|
-
"@etsoo/shared": "^1.1
|
|
55
|
+
"@etsoo/react": "^1.6.67",
|
|
56
|
+
"@etsoo/shared": "^1.2.1",
|
|
57
57
|
"@mui/icons-material": "^5.11.16",
|
|
58
|
-
"@mui/material": "^5.
|
|
59
|
-
"@mui/x-data-grid": "^6.0
|
|
58
|
+
"@mui/material": "^5.12.0",
|
|
59
|
+
"@mui/x-data-grid": "^6.2.0",
|
|
60
60
|
"@types/pica": "^9.0.1",
|
|
61
61
|
"@types/pulltorefreshjs": "^0.1.5",
|
|
62
|
-
"@types/react": "^18.0.
|
|
62
|
+
"@types/react": "^18.0.35",
|
|
63
63
|
"@types/react-avatar-editor": "^13.0.0",
|
|
64
64
|
"@types/react-dom": "^18.0.11",
|
|
65
65
|
"@types/react-input-mask": "^3.0.2",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"react-avatar-editor": "^13.0.0",
|
|
71
71
|
"react-dom": "^18.2.0",
|
|
72
72
|
"react-draggable": "^4.4.5",
|
|
73
|
-
"react-imask": "^6.5.
|
|
73
|
+
"react-imask": "^6.5.1",
|
|
74
74
|
"react-router-dom": "^6.10.0",
|
|
75
75
|
"react-window": "^1.8.8"
|
|
76
76
|
},
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
"@testing-library/jest-dom": "^5.16.5",
|
|
86
86
|
"@testing-library/react": "^14.0.0",
|
|
87
87
|
"@types/jest": "^29.5.0",
|
|
88
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
89
|
-
"@typescript-eslint/parser": "^5.
|
|
88
|
+
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
|
89
|
+
"@typescript-eslint/parser": "^5.58.0",
|
|
90
90
|
"jest": "^29.5.0",
|
|
91
91
|
"jest-environment-jsdom": "^29.5.0",
|
|
92
92
|
"typescript": "^5.0.4"
|
package/src/ComboBoxPro.tsx
CHANGED
|
@@ -44,9 +44,6 @@ export function ComboBoxPro<D extends ListType2 = ListType2>(
|
|
|
44
44
|
open: openDefault
|
|
45
45
|
} = globalApp?.getLabels("noOptions", "loading", "open") ?? {};
|
|
46
46
|
|
|
47
|
-
const getLabel = (item: D) =>
|
|
48
|
-
"label" in item ? item.label : "name" in item ? item.name : "";
|
|
49
|
-
|
|
50
47
|
// Destruct
|
|
51
48
|
const {
|
|
52
49
|
noOptionsText = noOptions,
|
|
@@ -121,7 +118,11 @@ export function ComboBoxPro<D extends ListType2 = ListType2>(
|
|
|
121
118
|
/>
|
|
122
119
|
)}
|
|
123
120
|
getOptionLabel={(item) =>
|
|
124
|
-
typeof item === "object"
|
|
121
|
+
typeof item === "object"
|
|
122
|
+
? "label" in item
|
|
123
|
+
? item.label
|
|
124
|
+
: item.name
|
|
125
|
+
: item
|
|
125
126
|
}
|
|
126
127
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
127
128
|
noOptionsText={noOptionsText}
|
package/src/SearchBar.tsx
CHANGED
package/src/Tiplist.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ReactUtils, useDelayedExecutor } from "@etsoo/react";
|
|
2
2
|
import { DataTypes, IdDefaultType, ListType } from "@etsoo/shared";
|
|
3
3
|
import { Autocomplete, AutocompleteRenderInputParams } from "@mui/material";
|
|
4
|
-
import { width } from "@mui/system";
|
|
5
4
|
import React from "react";
|
|
6
5
|
import { globalApp } from "./app/ReactApp";
|
|
7
6
|
import { AutocompleteExtendedProps } from "./AutocompleteExtendedProps";
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { ReactUtils, useDelayedExecutor } from "@etsoo/react";
|
|
2
|
+
import { ListType2 } from "@etsoo/shared";
|
|
3
|
+
import { Autocomplete, AutocompleteProps } from "@mui/material";
|
|
4
|
+
import React, { ChangeEventHandler } from "react";
|
|
5
|
+
import { InputField, InputFieldProps } from "./InputField";
|
|
6
|
+
import { globalApp } from "./app/ReactApp";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TiplistPro props
|
|
10
|
+
*/
|
|
11
|
+
export type TiplistProProps<T extends ListType2 = ListType2> = Omit<
|
|
12
|
+
AutocompleteProps<T, false, false, true>,
|
|
13
|
+
"open" | "multiple" | "options" | "renderInput"
|
|
14
|
+
> & {
|
|
15
|
+
/**
|
|
16
|
+
* Load data callback
|
|
17
|
+
*/
|
|
18
|
+
loadData: (
|
|
19
|
+
keyword: string | undefined,
|
|
20
|
+
id: T["id"] | undefined,
|
|
21
|
+
maxItems: number
|
|
22
|
+
) => PromiseLike<T[] | null | undefined>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Max items to read and display
|
|
26
|
+
*/
|
|
27
|
+
maxItems?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Width
|
|
31
|
+
*/
|
|
32
|
+
width?: number;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Label
|
|
36
|
+
*/
|
|
37
|
+
label?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Field name
|
|
41
|
+
*/
|
|
42
|
+
name?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Id value
|
|
46
|
+
*/
|
|
47
|
+
idValue?: T["id"] | null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Input onChange hanlder
|
|
51
|
+
*/
|
|
52
|
+
inputOnChange?: ChangeEventHandler<HTMLInputElement> | undefined;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Input props
|
|
56
|
+
*/
|
|
57
|
+
inputProps?: Omit<InputFieldProps, "onChange">;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Multiple states
|
|
61
|
+
interface States<T extends object> {
|
|
62
|
+
open: boolean;
|
|
63
|
+
options: T[];
|
|
64
|
+
value?: T | string | null | undefined;
|
|
65
|
+
loading?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* TiplistPro
|
|
70
|
+
* @param props Props
|
|
71
|
+
* @returns Component
|
|
72
|
+
*/
|
|
73
|
+
export function TiplistPro<T extends ListType2 = ListType2>(
|
|
74
|
+
props: TiplistProProps<T>
|
|
75
|
+
) {
|
|
76
|
+
// Labels
|
|
77
|
+
const {
|
|
78
|
+
noOptions,
|
|
79
|
+
loading,
|
|
80
|
+
more,
|
|
81
|
+
open: openDefault
|
|
82
|
+
} = globalApp?.getLabels("noOptions", "loading", "more", "open") ?? {};
|
|
83
|
+
|
|
84
|
+
// Destruct
|
|
85
|
+
const {
|
|
86
|
+
label,
|
|
87
|
+
loadData,
|
|
88
|
+
defaultValue,
|
|
89
|
+
value,
|
|
90
|
+
idValue,
|
|
91
|
+
maxItems = 16,
|
|
92
|
+
width,
|
|
93
|
+
name,
|
|
94
|
+
inputOnChange,
|
|
95
|
+
inputProps,
|
|
96
|
+
sx,
|
|
97
|
+
openOnFocus = true,
|
|
98
|
+
noOptionsText = noOptions,
|
|
99
|
+
loadingText = loading,
|
|
100
|
+
openText = openDefault,
|
|
101
|
+
getOptionDisabled,
|
|
102
|
+
getOptionLabel,
|
|
103
|
+
onChange,
|
|
104
|
+
...rest
|
|
105
|
+
} = props;
|
|
106
|
+
|
|
107
|
+
if (width && sx) Object.assign(sx, { width: `${width}px` });
|
|
108
|
+
|
|
109
|
+
// Value input ref
|
|
110
|
+
const inputRef = React.createRef<HTMLInputElement>();
|
|
111
|
+
|
|
112
|
+
// Local value
|
|
113
|
+
let localValue = value ?? defaultValue;
|
|
114
|
+
|
|
115
|
+
// One time calculation for input's default value (uncontrolled)
|
|
116
|
+
const localIdValue =
|
|
117
|
+
idValue ??
|
|
118
|
+
(localValue != null && typeof localValue === "object"
|
|
119
|
+
? localValue.id
|
|
120
|
+
: null);
|
|
121
|
+
|
|
122
|
+
// Changable states
|
|
123
|
+
const [states, stateUpdate] = React.useReducer(
|
|
124
|
+
(currentState: States<T>, newState: Partial<States<T>>) => {
|
|
125
|
+
return { ...currentState, ...newState };
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
// Loading unknown
|
|
129
|
+
open: false,
|
|
130
|
+
options: [],
|
|
131
|
+
value: null
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
if (localValue != value) stateUpdate({ value: localValue });
|
|
137
|
+
}, [localValue]);
|
|
138
|
+
|
|
139
|
+
// Input value
|
|
140
|
+
const inputValue = React.useMemo(
|
|
141
|
+
() =>
|
|
142
|
+
states.value && typeof states.value === "object"
|
|
143
|
+
? states.value.id
|
|
144
|
+
: undefined,
|
|
145
|
+
[states.value]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// State
|
|
149
|
+
const [state] = React.useState<{
|
|
150
|
+
idLoaded?: boolean;
|
|
151
|
+
idSet?: boolean;
|
|
152
|
+
}>({});
|
|
153
|
+
const isMounted = React.useRef(true);
|
|
154
|
+
|
|
155
|
+
// Change handler
|
|
156
|
+
const changeHandle = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
157
|
+
// Stop processing with auto trigger event
|
|
158
|
+
if (event.nativeEvent.cancelable && !event.nativeEvent.composed) {
|
|
159
|
+
stateUpdate({ options: [] });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Stop bubble
|
|
164
|
+
event.stopPropagation();
|
|
165
|
+
|
|
166
|
+
// Call with delay
|
|
167
|
+
delayed.call(undefined, event.currentTarget.value);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Directly load data
|
|
171
|
+
const loadDataDirect = (keyword?: string, id?: T["id"]) => {
|
|
172
|
+
// Reset options
|
|
173
|
+
// setOptions([]);
|
|
174
|
+
|
|
175
|
+
if (id == null) {
|
|
176
|
+
// Reset real value
|
|
177
|
+
const input = inputRef.current;
|
|
178
|
+
|
|
179
|
+
if (input && input.value !== "") {
|
|
180
|
+
// Different value, trigger change event
|
|
181
|
+
ReactUtils.triggerChange(input, "", false);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (states.options.length > 0) {
|
|
185
|
+
// Reset options
|
|
186
|
+
stateUpdate({ options: [] });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Loading indicator
|
|
191
|
+
if (!states.loading) stateUpdate({ loading: true });
|
|
192
|
+
|
|
193
|
+
// Load list
|
|
194
|
+
loadData(keyword, id, maxItems).then((options) => {
|
|
195
|
+
if (!isMounted.current) return;
|
|
196
|
+
|
|
197
|
+
if (options != null && options.length >= maxItems) {
|
|
198
|
+
options.push({ id: -1, name: "n/a" } as T);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Indicates loading completed
|
|
202
|
+
stateUpdate({
|
|
203
|
+
loading: false,
|
|
204
|
+
...(options != null && { options })
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const delayed = useDelayedExecutor(loadDataDirect, 480);
|
|
210
|
+
|
|
211
|
+
const setInputValue = (value: T | null) => {
|
|
212
|
+
stateUpdate({ value });
|
|
213
|
+
|
|
214
|
+
// Input value
|
|
215
|
+
const input = inputRef.current;
|
|
216
|
+
if (input) {
|
|
217
|
+
// Update value
|
|
218
|
+
const newValue = value?.id.toString() ?? "";
|
|
219
|
+
if (newValue !== input.value) {
|
|
220
|
+
// Different value, trigger change event
|
|
221
|
+
ReactUtils.triggerChange(input, newValue, false);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (localIdValue != null && (localIdValue as any) !== "") {
|
|
227
|
+
if (state.idLoaded) {
|
|
228
|
+
// Set default
|
|
229
|
+
if (!state.idSet && states.options.length == 1) {
|
|
230
|
+
stateUpdate({ value: states.options[0] });
|
|
231
|
+
state.idSet = true;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// Load id data
|
|
235
|
+
loadDataDirect(undefined, localIdValue);
|
|
236
|
+
state.idLoaded = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
React.useEffect(() => {
|
|
241
|
+
return () => {
|
|
242
|
+
isMounted.current = false;
|
|
243
|
+
delayed.clear();
|
|
244
|
+
};
|
|
245
|
+
}, []);
|
|
246
|
+
|
|
247
|
+
// Layout
|
|
248
|
+
return (
|
|
249
|
+
<div>
|
|
250
|
+
<input
|
|
251
|
+
ref={inputRef}
|
|
252
|
+
data-reset="true"
|
|
253
|
+
type="text"
|
|
254
|
+
style={{ display: "none" }}
|
|
255
|
+
name={name}
|
|
256
|
+
value={inputValue ?? ""}
|
|
257
|
+
readOnly
|
|
258
|
+
onChange={inputOnChange}
|
|
259
|
+
/>
|
|
260
|
+
{/* Previous input will reset first with "disableClearable = false", next input trigger change works */}
|
|
261
|
+
<Autocomplete<T, false, false, true>
|
|
262
|
+
filterOptions={(options, _state) => options}
|
|
263
|
+
value={states.value}
|
|
264
|
+
options={states.options}
|
|
265
|
+
freeSolo
|
|
266
|
+
clearOnBlur={false}
|
|
267
|
+
onChange={(event, value, reason, details) => {
|
|
268
|
+
if (typeof value === "object") {
|
|
269
|
+
// Set value
|
|
270
|
+
setInputValue(value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Custom
|
|
274
|
+
if (onChange != null) onChange(event, value, reason, details);
|
|
275
|
+
|
|
276
|
+
// For clear case
|
|
277
|
+
if (reason === "clear") {
|
|
278
|
+
stateUpdate({ options: [] });
|
|
279
|
+
loadDataDirect();
|
|
280
|
+
}
|
|
281
|
+
}}
|
|
282
|
+
open={states.open}
|
|
283
|
+
openOnFocus={openOnFocus}
|
|
284
|
+
onOpen={() => {
|
|
285
|
+
// Should load
|
|
286
|
+
const loading = states.loading ? true : states.options.length === 0;
|
|
287
|
+
|
|
288
|
+
stateUpdate({ open: true, loading });
|
|
289
|
+
|
|
290
|
+
// If not loading
|
|
291
|
+
if (loading)
|
|
292
|
+
loadDataDirect(
|
|
293
|
+
undefined,
|
|
294
|
+
states.value && typeof states.value === "object"
|
|
295
|
+
? states.value.id
|
|
296
|
+
: undefined
|
|
297
|
+
);
|
|
298
|
+
}}
|
|
299
|
+
onClose={() => {
|
|
300
|
+
stateUpdate({
|
|
301
|
+
open: false,
|
|
302
|
+
...(!states.value && { options: [] })
|
|
303
|
+
});
|
|
304
|
+
}}
|
|
305
|
+
loading={states.loading}
|
|
306
|
+
renderInput={(params) => (
|
|
307
|
+
<InputField
|
|
308
|
+
{...inputProps}
|
|
309
|
+
{...params}
|
|
310
|
+
onChange={changeHandle}
|
|
311
|
+
label={label}
|
|
312
|
+
name={name + "Input"}
|
|
313
|
+
onBlur={(event) => {
|
|
314
|
+
if (states.value == null && onChange)
|
|
315
|
+
onChange(event, event.target.value, "blur", undefined);
|
|
316
|
+
}}
|
|
317
|
+
/>
|
|
318
|
+
)}
|
|
319
|
+
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
320
|
+
sx={sx}
|
|
321
|
+
noOptionsText={noOptionsText}
|
|
322
|
+
loadingText={loadingText}
|
|
323
|
+
openText={openText}
|
|
324
|
+
getOptionDisabled={(item) => {
|
|
325
|
+
if (item.id === -1) return true;
|
|
326
|
+
return getOptionDisabled ? getOptionDisabled(item) : false;
|
|
327
|
+
}}
|
|
328
|
+
getOptionLabel={(item) => {
|
|
329
|
+
if (typeof item === "string") return item;
|
|
330
|
+
if (item["id"] === -1) return (more ?? "More") + "...";
|
|
331
|
+
if (getOptionLabel == null)
|
|
332
|
+
return "label" in item ? item.label : item.name;
|
|
333
|
+
return getOptionLabel(item);
|
|
334
|
+
}}
|
|
335
|
+
{...rest}
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
}
|
package/src/index.ts
CHANGED