@etsoo/materialui 1.1.25 → 1.1.27
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/ComboBox.d.ts +2 -2
- package/lib/ComboBox.js +19 -16
- package/lib/Tiplist.d.ts +3 -3
- package/lib/Tiplist.js +19 -20
- package/lib/pages/EditPage.d.ts +7 -3
- package/lib/pages/EditPage.js +11 -10
- package/package.json +6 -6
- package/src/ComboBox.tsx +267 -260
- package/src/Tiplist.tsx +267 -264
- package/src/pages/EditPage.tsx +93 -95
package/src/Tiplist.tsx
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
import { ReactUtils, useDelayedExecutor } from
|
|
2
|
-
import { DataTypes, IdDefaultType, ListType } from
|
|
3
|
-
import { Autocomplete, AutocompleteRenderInputParams } from
|
|
4
|
-
import React from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { ReactUtils, useDelayedExecutor } from "@etsoo/react";
|
|
2
|
+
import { DataTypes, IdDefaultType, ListType } from "@etsoo/shared";
|
|
3
|
+
import { Autocomplete, AutocompleteRenderInputParams } from "@mui/material";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { globalApp } from "./app/ReactApp";
|
|
6
|
+
import { AutocompleteExtendedProps } from "./AutocompleteExtendedProps";
|
|
7
|
+
import { InputField } from "./InputField";
|
|
8
|
+
import { SearchField } from "./SearchField";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Tiplist props
|
|
11
12
|
*/
|
|
12
13
|
export type TiplistProps<T extends object, D extends DataTypes.Keys<T>> = Omit<
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
AutocompleteExtendedProps<T, D, undefined>,
|
|
15
|
+
"open" | "multiple"
|
|
15
16
|
> & {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Load data callback
|
|
19
|
+
*/
|
|
20
|
+
loadData: (
|
|
21
|
+
keyword?: string,
|
|
22
|
+
id?: T[D]
|
|
23
|
+
) => PromiseLike<T[] | null | undefined>;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
// Multiple states
|
|
26
27
|
interface States<T extends object> {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
open: boolean;
|
|
29
|
+
options: T[];
|
|
30
|
+
value?: T | null;
|
|
31
|
+
loading?: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -36,268 +37,270 @@ interface States<T extends object> {
|
|
|
36
37
|
* @returns Component
|
|
37
38
|
*/
|
|
38
39
|
export function Tiplist<
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
T extends object = ListType,
|
|
41
|
+
D extends DataTypes.Keys<T> = IdDefaultType<T>
|
|
41
42
|
>(props: TiplistProps<T, D>) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
43
|
+
// Labels
|
|
44
|
+
const labels = globalApp?.getLabels("noOptions", "loading");
|
|
45
|
+
|
|
46
|
+
// Destruct
|
|
47
|
+
const {
|
|
48
|
+
search = false,
|
|
49
|
+
idField = "id" as D,
|
|
50
|
+
idValue,
|
|
51
|
+
inputAutoComplete = "new-password",
|
|
52
|
+
inputError,
|
|
53
|
+
inputHelperText,
|
|
54
|
+
inputMargin,
|
|
55
|
+
inputOnChange,
|
|
56
|
+
inputRequired,
|
|
57
|
+
inputVariant,
|
|
58
|
+
label,
|
|
59
|
+
loadData,
|
|
60
|
+
defaultValue,
|
|
61
|
+
value,
|
|
62
|
+
name,
|
|
63
|
+
readOnly,
|
|
64
|
+
onChange,
|
|
65
|
+
openOnFocus = true,
|
|
66
|
+
sx = { minWidth: "180px" },
|
|
67
|
+
noOptionsText = labels?.noOptions,
|
|
68
|
+
loadingText = labels?.loading,
|
|
69
|
+
...rest
|
|
70
|
+
} = props;
|
|
71
|
+
|
|
72
|
+
// Value input ref
|
|
73
|
+
const inputRef = React.createRef<HTMLInputElement>();
|
|
74
|
+
|
|
75
|
+
// Local value
|
|
76
|
+
let localValue = value ?? defaultValue;
|
|
77
|
+
|
|
78
|
+
// One time calculation for input's default value (uncontrolled)
|
|
79
|
+
const localIdValue =
|
|
80
|
+
idValue ?? DataTypes.getValue(localValue, idField as any);
|
|
81
|
+
|
|
82
|
+
// Changable states
|
|
83
|
+
const [states, stateUpdate] = React.useReducer(
|
|
84
|
+
(currentState: States<T>, newState: Partial<States<T>>) => {
|
|
85
|
+
return { ...currentState, ...newState };
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
// Loading unknown
|
|
89
|
+
open: false,
|
|
90
|
+
options: [],
|
|
91
|
+
value: null
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Input value
|
|
96
|
+
const inputValue = React.useMemo(
|
|
97
|
+
() => states.value && states.value[idField],
|
|
98
|
+
[states.value]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
React.useEffect(() => {
|
|
102
|
+
if (localValue != null) stateUpdate({ value: localValue });
|
|
103
|
+
}, [localValue]);
|
|
104
|
+
|
|
105
|
+
// State
|
|
106
|
+
const [state] = React.useState<{
|
|
107
|
+
idLoaded?: boolean;
|
|
108
|
+
idSet?: boolean;
|
|
109
|
+
}>({});
|
|
110
|
+
const isMounted = React.useRef(true);
|
|
111
|
+
|
|
112
|
+
// Add readOnly
|
|
113
|
+
const addReadOnly = (params: AutocompleteRenderInputParams) => {
|
|
114
|
+
if (readOnly != null) {
|
|
115
|
+
Object.assign(params, { readOnly });
|
|
116
|
+
}
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
// https://stackoverflow.com/questions/15738259/disabling-chrome-autofill
|
|
119
|
+
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html
|
|
120
|
+
Object.assign(params.inputProps, { autoComplete: inputAutoComplete });
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Stop processing with auto trigger event
|
|
122
|
-
if (event.nativeEvent.cancelable && !event.nativeEvent.composed) {
|
|
123
|
-
stateUpdate({ options: [] });
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
122
|
+
return params;
|
|
123
|
+
};
|
|
126
124
|
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
// Change handler
|
|
126
|
+
const changeHandle = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
127
|
+
// Stop processing with auto trigger event
|
|
128
|
+
if (event.nativeEvent.cancelable && !event.nativeEvent.composed) {
|
|
129
|
+
stateUpdate({ options: [] });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
129
132
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
};
|
|
133
|
+
// Stop bubble
|
|
134
|
+
event.stopPropagation();
|
|
133
135
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// setOptions([]);
|
|
136
|
+
// Call with delay
|
|
137
|
+
delayed.call(undefined, event.currentTarget.value);
|
|
138
|
+
};
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// Directly load data
|
|
141
|
+
const loadDataDirect = (keyword?: string, id?: T[D]) => {
|
|
142
|
+
// Reset options
|
|
143
|
+
// setOptions([]);
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
145
|
+
if (id == null) {
|
|
146
|
+
// Reset real value
|
|
147
|
+
const input = inputRef.current;
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
149
|
+
if (input && input.value !== "") {
|
|
150
|
+
// Different value, trigger change event
|
|
151
|
+
ReactUtils.triggerChange(input, "", false);
|
|
152
|
+
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
if (states.options.length > 0) {
|
|
155
|
+
// Reset options
|
|
156
|
+
stateUpdate({ options: [] });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
156
159
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!isMounted.current) return;
|
|
160
|
+
// Loading indicator
|
|
161
|
+
if (!states.loading) stateUpdate({ loading: true });
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
...(options != null && { options })
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
};
|
|
163
|
+
// Load list
|
|
164
|
+
loadData(keyword, id).then((options) => {
|
|
165
|
+
if (!isMounted.current) return;
|
|
168
166
|
|
|
169
|
-
|
|
167
|
+
// Indicates loading completed
|
|
168
|
+
stateUpdate({
|
|
169
|
+
loading: false,
|
|
170
|
+
...(options != null && { options })
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
};
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
stateUpdate({ value });
|
|
175
|
+
const delayed = useDelayedExecutor(loadDataDirect, 480);
|
|
173
176
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (input) {
|
|
177
|
-
// Update value
|
|
178
|
-
const newValue = DataTypes.getStringValue(value, idField) ?? '';
|
|
179
|
-
if (newValue !== input.value) {
|
|
180
|
-
// Different value, trigger change event
|
|
181
|
-
ReactUtils.triggerChange(input, newValue, false);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
};
|
|
177
|
+
const setInputValue = (value: T | null) => {
|
|
178
|
+
stateUpdate({ value });
|
|
185
179
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
180
|
+
// Input value
|
|
181
|
+
const input = inputRef.current;
|
|
182
|
+
if (input) {
|
|
183
|
+
// Update value
|
|
184
|
+
const newValue = DataTypes.getStringValue(value, idField) ?? "";
|
|
185
|
+
if (newValue !== input.value) {
|
|
186
|
+
// Different value, trigger change event
|
|
187
|
+
ReactUtils.triggerChange(input, newValue, false);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if (localIdValue != null && (localIdValue as any) !== "") {
|
|
193
|
+
if (state.idLoaded) {
|
|
194
|
+
// Set default
|
|
195
|
+
if (!state.idSet && states.options.length == 1) {
|
|
196
|
+
stateUpdate({ value: states.options[0] });
|
|
197
|
+
state.idSet = true;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
// Load id data
|
|
201
|
+
loadDataDirect(undefined, localIdValue);
|
|
202
|
+
state.idLoaded = true;
|
|
198
203
|
}
|
|
204
|
+
}
|
|
199
205
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
206
|
+
React.useEffect(() => {
|
|
207
|
+
return () => {
|
|
208
|
+
isMounted.current = false;
|
|
209
|
+
delayed.clear();
|
|
210
|
+
};
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
// Layout
|
|
214
|
+
return (
|
|
215
|
+
<div>
|
|
216
|
+
<input
|
|
217
|
+
ref={inputRef}
|
|
218
|
+
data-reset="true"
|
|
219
|
+
type="text"
|
|
220
|
+
style={{ display: "none" }}
|
|
221
|
+
name={name}
|
|
222
|
+
value={`${inputValue ?? ""}`}
|
|
223
|
+
readOnly
|
|
224
|
+
onChange={inputOnChange}
|
|
225
|
+
/>
|
|
226
|
+
{/* Previous input will reset first with "disableClearable = false", next input trigger change works */}
|
|
227
|
+
<Autocomplete<T, undefined, false, false>
|
|
228
|
+
filterOptions={(options, _state) => options}
|
|
229
|
+
value={states.value}
|
|
230
|
+
options={states.options}
|
|
231
|
+
onChange={(event, value, reason, details) => {
|
|
232
|
+
// Set value
|
|
233
|
+
setInputValue(value);
|
|
234
|
+
|
|
235
|
+
// Custom
|
|
236
|
+
if (onChange != null) onChange(event, value, reason, details);
|
|
237
|
+
|
|
238
|
+
// For clear case
|
|
239
|
+
if (reason === "clear") {
|
|
240
|
+
stateUpdate({ options: [] });
|
|
241
|
+
loadDataDirect();
|
|
242
|
+
}
|
|
243
|
+
}}
|
|
244
|
+
open={states.open}
|
|
245
|
+
openOnFocus={openOnFocus}
|
|
246
|
+
onOpen={() => {
|
|
247
|
+
// Should load
|
|
248
|
+
const loading = states.loading ? true : states.options.length === 0;
|
|
249
|
+
|
|
250
|
+
stateUpdate({ open: true, loading });
|
|
251
|
+
|
|
252
|
+
// If not loading
|
|
253
|
+
if (loading)
|
|
254
|
+
loadDataDirect(
|
|
255
|
+
undefined,
|
|
256
|
+
states.value == null ? undefined : states.value[idField]
|
|
257
|
+
);
|
|
258
|
+
}}
|
|
259
|
+
onClose={() => {
|
|
260
|
+
stateUpdate({
|
|
261
|
+
open: false,
|
|
262
|
+
...(!states.value && { options: [] })
|
|
263
|
+
});
|
|
264
|
+
}}
|
|
265
|
+
loading={states.loading}
|
|
266
|
+
sx={sx}
|
|
267
|
+
renderInput={(params) =>
|
|
268
|
+
search ? (
|
|
269
|
+
<SearchField
|
|
270
|
+
onChange={changeHandle}
|
|
271
|
+
{...addReadOnly(params)}
|
|
272
|
+
readOnly={readOnly}
|
|
273
|
+
label={label}
|
|
274
|
+
name={name + "Input"}
|
|
275
|
+
margin={inputMargin}
|
|
276
|
+
variant={inputVariant}
|
|
277
|
+
required={inputRequired}
|
|
278
|
+
autoComplete={inputAutoComplete}
|
|
279
|
+
error={inputError}
|
|
280
|
+
helperText={inputHelperText}
|
|
219
281
|
/>
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// For clear case
|
|
234
|
-
if (reason === 'clear') {
|
|
235
|
-
stateUpdate({ options: [] });
|
|
236
|
-
loadDataDirect();
|
|
237
|
-
}
|
|
238
|
-
}}
|
|
239
|
-
open={states.open}
|
|
240
|
-
openOnFocus={openOnFocus}
|
|
241
|
-
onOpen={() => {
|
|
242
|
-
// Should load
|
|
243
|
-
const loading = states.loading
|
|
244
|
-
? true
|
|
245
|
-
: states.options.length === 0;
|
|
246
|
-
|
|
247
|
-
stateUpdate({ open: true, loading });
|
|
248
|
-
|
|
249
|
-
// If not loading
|
|
250
|
-
if (loading)
|
|
251
|
-
loadDataDirect(
|
|
252
|
-
undefined,
|
|
253
|
-
states.value == null
|
|
254
|
-
? undefined
|
|
255
|
-
: states.value[idField]
|
|
256
|
-
);
|
|
257
|
-
}}
|
|
258
|
-
onClose={() => {
|
|
259
|
-
stateUpdate({
|
|
260
|
-
open: false,
|
|
261
|
-
...(!states.value && { options: [] })
|
|
262
|
-
});
|
|
263
|
-
}}
|
|
264
|
-
loading={states.loading}
|
|
265
|
-
sx={sx}
|
|
266
|
-
renderInput={(params) =>
|
|
267
|
-
search ? (
|
|
268
|
-
<SearchField
|
|
269
|
-
onChange={changeHandle}
|
|
270
|
-
{...addReadOnly(params)}
|
|
271
|
-
readOnly={readOnly}
|
|
272
|
-
label={label}
|
|
273
|
-
name={name + 'Input'}
|
|
274
|
-
margin={inputMargin}
|
|
275
|
-
variant={inputVariant}
|
|
276
|
-
required={inputRequired}
|
|
277
|
-
autoComplete={inputAutoComplete}
|
|
278
|
-
error={inputError}
|
|
279
|
-
helperText={inputHelperText}
|
|
280
|
-
/>
|
|
281
|
-
) : (
|
|
282
|
-
<InputField
|
|
283
|
-
onChange={changeHandle}
|
|
284
|
-
{...addReadOnly(params)}
|
|
285
|
-
label={label}
|
|
286
|
-
name={name + 'Input'}
|
|
287
|
-
margin={inputMargin}
|
|
288
|
-
variant={inputVariant}
|
|
289
|
-
required={inputRequired}
|
|
290
|
-
autoComplete={inputAutoComplete}
|
|
291
|
-
error={inputError}
|
|
292
|
-
helperText={inputHelperText}
|
|
293
|
-
/>
|
|
294
|
-
)
|
|
295
|
-
}
|
|
296
|
-
isOptionEqualToValue={(option: T, value: T) =>
|
|
297
|
-
option[idField] === value[idField]
|
|
298
|
-
}
|
|
299
|
-
{...rest}
|
|
282
|
+
) : (
|
|
283
|
+
<InputField
|
|
284
|
+
onChange={changeHandle}
|
|
285
|
+
{...addReadOnly(params)}
|
|
286
|
+
label={label}
|
|
287
|
+
name={name + "Input"}
|
|
288
|
+
margin={inputMargin}
|
|
289
|
+
variant={inputVariant}
|
|
290
|
+
required={inputRequired}
|
|
291
|
+
autoComplete={inputAutoComplete}
|
|
292
|
+
error={inputError}
|
|
293
|
+
helperText={inputHelperText}
|
|
300
294
|
/>
|
|
301
|
-
|
|
302
|
-
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
isOptionEqualToValue={(option: T, value: T) =>
|
|
298
|
+
option[idField] === value[idField]
|
|
299
|
+
}
|
|
300
|
+
noOptionsText={noOptionsText}
|
|
301
|
+
loadingText={loadingText}
|
|
302
|
+
{...rest}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
303
306
|
}
|