@etsoo/materialui 1.1.71 → 1.1.73

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/src/SelectEx.tsx CHANGED
@@ -1,106 +1,107 @@
1
1
  import {
2
- Checkbox,
3
- FormControl,
4
- FormHelperText,
5
- IconButton,
6
- InputLabel,
7
- ListItemText,
8
- MenuItem,
9
- OutlinedInput,
10
- Select,
11
- SelectProps,
12
- Stack
13
- } from '@mui/material';
14
- import React from 'react';
15
- import { MUGlobal } from './MUGlobal';
16
- import { ListItemRightIcon } from './ListItemRightIcon';
17
- import RefreshIcon from '@mui/icons-material/Refresh';
2
+ Checkbox,
3
+ FormControl,
4
+ FormHelperText,
5
+ IconButton,
6
+ InputLabel,
7
+ ListItemText,
8
+ MenuItem,
9
+ OutlinedInput,
10
+ Select,
11
+ SelectProps,
12
+ Stack
13
+ } from "@mui/material";
14
+ import React from "react";
15
+ import { MUGlobal } from "./MUGlobal";
16
+ import { ListItemRightIcon } from "./ListItemRightIcon";
17
+ import RefreshIcon from "@mui/icons-material/Refresh";
18
18
  import {
19
- DataTypes,
20
- IdDefaultType,
21
- LabelDefaultType,
22
- ListType,
23
- Utils
24
- } from '@etsoo/shared';
25
- import { ReactUtils } from '@etsoo/react';
19
+ ArrayUtils,
20
+ DataTypes,
21
+ IdDefaultType,
22
+ LabelDefaultType,
23
+ ListType,
24
+ Utils
25
+ } from "@etsoo/shared";
26
+ import { ReactUtils } from "@etsoo/react";
26
27
 
27
28
  /**
28
29
  * Extended select component props
29
30
  */
30
31
  export type SelectExProps<
31
- T extends object,
32
- D extends DataTypes.Keys<T> = IdDefaultType<T>,
33
- L extends DataTypes.Keys<T, string> = LabelDefaultType<T>
34
- > = Omit<SelectProps, 'labelId' | 'input' | 'native'> & {
35
- /**
36
- * Auto add blank item
37
- */
38
- autoAddBlankItem?: boolean;
39
-
40
- /**
41
- * The helper text content.
42
- */
43
- helperText?: React.ReactNode;
44
-
45
- /**
46
- * Input required
47
- */
48
- inputRequired?: boolean;
49
-
50
- /**
51
- * Id field
52
- */
53
- idField?: D;
54
-
55
- /**
56
- * Item icon renderer
57
- */
58
- itemIconRenderer?: (id: T[D]) => React.ReactNode;
59
-
60
- /**
61
- * Item style
62
- */
63
- itemStyle?: (option: T) => React.CSSProperties;
64
-
65
- /**
66
- * Label field
67
- */
68
- labelField?: L | ((option: T) => string);
69
-
70
- /**
71
- * Load data callback
72
- */
73
- loadData?: () => PromiseLike<T[] | null | undefined>;
74
-
75
- /**
76
- * Item change callback
77
- */
78
- onItemChange?: (option: T | undefined, userAction: boolean) => void;
79
-
80
- /**
81
- * Item click handler
82
- */
83
- onItemClick?: (event: React.MouseEvent, option: T) => void;
84
-
85
- /**
86
- * On load data handler
87
- */
88
- onLoadData?: (options: T[]) => void;
89
-
90
- /**
91
- * Array of options.
92
- */
93
- options?: ReadonlyArray<T>;
94
-
95
- /**
96
- * Supports refresh label or component
97
- */
98
- refresh?: string | React.ReactNode;
99
-
100
- /**
101
- * Is search case?
102
- */
103
- search?: boolean;
32
+ T extends object,
33
+ D extends DataTypes.Keys<T> = IdDefaultType<T>,
34
+ L extends DataTypes.Keys<T, string> = LabelDefaultType<T>
35
+ > = Omit<SelectProps, "labelId" | "input" | "native"> & {
36
+ /**
37
+ * Auto add blank item
38
+ */
39
+ autoAddBlankItem?: boolean;
40
+
41
+ /**
42
+ * The helper text content.
43
+ */
44
+ helperText?: React.ReactNode;
45
+
46
+ /**
47
+ * Input required
48
+ */
49
+ inputRequired?: boolean;
50
+
51
+ /**
52
+ * Id field
53
+ */
54
+ idField?: D;
55
+
56
+ /**
57
+ * Item icon renderer
58
+ */
59
+ itemIconRenderer?: (id: T[D]) => React.ReactNode;
60
+
61
+ /**
62
+ * Item style
63
+ */
64
+ itemStyle?: (option: T) => React.CSSProperties;
65
+
66
+ /**
67
+ * Label field
68
+ */
69
+ labelField?: L | ((option: T) => string);
70
+
71
+ /**
72
+ * Load data callback
73
+ */
74
+ loadData?: () => PromiseLike<T[] | null | undefined>;
75
+
76
+ /**
77
+ * Item change callback
78
+ */
79
+ onItemChange?: (option: T | undefined, userAction: boolean) => void;
80
+
81
+ /**
82
+ * Item click handler
83
+ */
84
+ onItemClick?: (event: React.MouseEvent, option: T) => void;
85
+
86
+ /**
87
+ * On load data handler
88
+ */
89
+ onLoadData?: (options: T[]) => void;
90
+
91
+ /**
92
+ * Array of options.
93
+ */
94
+ options?: ReadonlyArray<T>;
95
+
96
+ /**
97
+ * Supports refresh label or component
98
+ */
99
+ refresh?: string | React.ReactNode;
100
+
101
+ /**
102
+ * Is search case?
103
+ */
104
+ search?: boolean;
104
105
  };
105
106
 
106
107
  /**
@@ -109,301 +110,277 @@ export type SelectExProps<
109
110
  * @returns Component
110
111
  */
111
112
  export function SelectEx<
112
- T extends object = ListType,
113
- D extends DataTypes.Keys<T> = IdDefaultType<T>,
114
- L extends DataTypes.Keys<T, string> = LabelDefaultType<T>
113
+ T extends object = ListType,
114
+ D extends DataTypes.Keys<T> = IdDefaultType<T>,
115
+ L extends DataTypes.Keys<T, string> = LabelDefaultType<T>
115
116
  >(props: SelectExProps<T, D, L>) {
116
- // Destruct
117
- const {
118
- defaultValue,
119
- idField = 'id' as D,
120
- error,
121
- helperText,
122
- inputRequired,
123
- itemIconRenderer,
124
- itemStyle,
125
- label,
126
- labelField = 'label' as L,
127
- loadData,
128
- onItemChange,
129
- onItemClick,
130
- onLoadData,
131
- multiple = false,
132
- name,
133
- options,
134
- refresh,
135
- search = false,
136
- autoAddBlankItem = search,
137
- value,
138
- onChange,
139
- fullWidth,
140
- ...rest
141
- } = props;
142
-
143
- // Options state
144
- const [localOptions, setOptions] = React.useState<readonly T[]>([]);
145
- const isMounted = React.useRef(false);
146
-
147
- const doItemChange = (
148
- options: readonly T[],
149
- value: unknown,
150
- userAction: boolean
151
- ) => {
152
- if (onItemChange == null) return;
153
-
154
- let option: T | undefined;
155
- if (multiple && Array.isArray(value)) {
156
- option = options.find((option) => value.includes(option[idField]));
157
- } else if (value == null || value === '') {
158
- option = undefined;
159
- } else {
160
- option = options.find((option) => option[idField] === value);
161
- }
162
- onItemChange(option, userAction);
117
+ // Destruct
118
+ const {
119
+ defaultValue,
120
+ idField = "id" as D,
121
+ error,
122
+ helperText,
123
+ inputRequired,
124
+ itemIconRenderer,
125
+ itemStyle,
126
+ label,
127
+ labelField = "label" as L,
128
+ loadData,
129
+ onItemChange,
130
+ onItemClick,
131
+ onLoadData,
132
+ multiple = false,
133
+ name,
134
+ options,
135
+ refresh,
136
+ search = false,
137
+ autoAddBlankItem = search,
138
+ value,
139
+ onChange,
140
+ fullWidth,
141
+ ...rest
142
+ } = props;
143
+
144
+ // Options state
145
+ const [localOptions, setOptions] = React.useState<readonly T[]>([]);
146
+ const isMounted = React.useRef(false);
147
+
148
+ const doItemChange = (
149
+ options: readonly T[],
150
+ value: unknown,
151
+ userAction: boolean
152
+ ) => {
153
+ if (onItemChange == null) return;
154
+
155
+ let option: T | undefined;
156
+ if (multiple && Array.isArray(value)) {
157
+ option = options.find((option) => value.includes(option[idField]));
158
+ } else if (value == null || value === "") {
159
+ option = undefined;
160
+ } else {
161
+ option = options.find((option) => option[idField] === value);
162
+ }
163
+ onItemChange(option, userAction);
164
+ };
165
+
166
+ const setOptionsAdd = (options: readonly T[]) => {
167
+ setOptions(options);
168
+ if (valueSource != null) doItemChange(options, valueSource, false);
169
+ };
170
+
171
+ // When options change
172
+ // [options] will cause infinite loop
173
+ const propertyWay = loadData == null;
174
+ React.useEffect(() => {
175
+ if (options == null || !propertyWay) return;
176
+ setOptionsAdd(options);
177
+ }, [options, propertyWay]);
178
+
179
+ // Local value
180
+ const v = defaultValue ?? value;
181
+ const valueSource = React.useMemo(
182
+ () => (multiple ? (v ? (Array.isArray(v) ? v : [v]) : []) : v ?? ""),
183
+ [multiple, v]
184
+ );
185
+
186
+ // Value state
187
+ const [valueState, setValueStateBase] = React.useState<unknown>(valueSource);
188
+ const valueRef = React.useRef<unknown>();
189
+ const setValueState = (newValue: unknown) => {
190
+ valueRef.current = newValue;
191
+ setValueStateBase(newValue);
192
+ };
193
+
194
+ React.useEffect(() => {
195
+ if (valueSource != null) setValueState(valueSource);
196
+ }, [valueSource]);
197
+
198
+ // Label id
199
+ const labelId = `selectex-label-${name}`;
200
+
201
+ // Set item
202
+ const setItemValue = (id: unknown) => {
203
+ if (id !== valueRef.current) {
204
+ // Difference
205
+ const diff = multiple
206
+ ? ArrayUtils.differences(id as T[D][], valueRef.current as T[D][])
207
+ : id;
208
+
209
+ setValueState(id);
210
+
211
+ const input = divRef.current?.querySelector("input");
212
+ if (input) {
213
+ // Different value, trigger change event
214
+ ReactUtils.triggerChange(input, id as string, false);
215
+ }
216
+ return diff;
217
+ }
218
+ return undefined;
219
+ };
220
+
221
+ // Get option id
222
+ const getId = (option: T) => {
223
+ return option[idField] as unknown as React.Key;
224
+ };
225
+
226
+ // Get option label
227
+ const getLabel = (option: T) => {
228
+ return typeof labelField === "function"
229
+ ? labelField(option)
230
+ : (option[labelField] as string);
231
+ };
232
+
233
+ // Refs
234
+ const divRef = React.useRef<HTMLDivElement>();
235
+
236
+ // Refresh list data
237
+ const refreshData = () => {
238
+ if (loadData == null) return;
239
+ loadData().then((result) => {
240
+ if (result == null || !isMounted.current) return;
241
+ if (onLoadData) onLoadData(result);
242
+ if (autoAddBlankItem) {
243
+ Utils.addBlankItem(result, idField, labelField);
244
+ }
245
+ setOptionsAdd(result);
246
+ });
247
+ };
248
+
249
+ // When value change
250
+ React.useEffect(() => {
251
+ refreshData();
252
+ }, [valueSource]);
253
+
254
+ // When layout ready
255
+ React.useEffect(() => {
256
+ const input = divRef.current?.querySelector("input");
257
+ const inputChange = (event: Event) => {
258
+ // Reset case
259
+ if (event.cancelable) setValueState(multiple ? [] : "");
163
260
  };
261
+ input?.addEventListener("change", inputChange);
164
262
 
165
- const setOptionsAdd = (options: readonly T[]) => {
166
- setOptions(options);
167
- if (valueSource != null) doItemChange(options, valueSource, false);
168
- };
263
+ isMounted.current = true;
169
264
 
170
- // When options change
171
- // [options] will cause infinite loop
172
- const propertyWay = loadData == null;
173
- React.useEffect(() => {
174
- if (options == null || !propertyWay) return;
175
- setOptionsAdd(options);
176
- }, [options, propertyWay]);
177
-
178
- // Local value
179
- const v = defaultValue ?? value;
180
- const valueSource = React.useMemo(
181
- () => (multiple ? (v ? (Array.isArray(v) ? v : [v]) : []) : v ?? ''),
182
- [multiple, v]
183
- );
184
-
185
- // Value state
186
- const [valueState, setValueStateBase] =
187
- React.useState<unknown>(valueSource);
188
- const valueRef = React.useRef<unknown>();
189
- const setValueState = (newValue: unknown) => {
190
- valueRef.current = newValue;
191
- setValueStateBase(newValue);
265
+ return () => {
266
+ isMounted.current = false;
267
+ input?.removeEventListener("change", inputChange);
192
268
  };
193
-
194
- React.useEffect(() => {
195
- if (valueSource != null) setValueState(valueSource);
196
- }, [valueSource]);
197
-
198
- // Label id
199
- const labelId = `selectex-label-${name}`;
200
-
201
- // Set item
202
- const setItemValue = (id: unknown) => {
203
- if (id !== valueRef.current) {
204
- // Difference
205
- const diff = multiple
206
- ? Utils.arrayDifferences(
207
- id as T[D][],
208
- valueRef.current as T[D][]
209
- )
210
- : id;
211
-
212
- setValueState(id);
213
-
214
- const input = divRef.current?.querySelector('input');
215
- if (input) {
216
- // Different value, trigger change event
217
- ReactUtils.triggerChange(input, id as string, false);
269
+ }, [multiple]);
270
+
271
+ // Layout
272
+ return (
273
+ <Stack direction="row">
274
+ <FormControl
275
+ size={search ? MUGlobal.searchFieldSize : MUGlobal.inputFieldSize}
276
+ fullWidth={fullWidth}
277
+ error={error}
278
+ >
279
+ <InputLabel
280
+ id={labelId}
281
+ shrink={
282
+ search ? MUGlobal.searchFieldShrink : MUGlobal.inputFieldShrink
283
+ }
284
+ >
285
+ {label}
286
+ </InputLabel>
287
+ <Select
288
+ ref={divRef}
289
+ value={
290
+ multiple
291
+ ? valueState
292
+ : localOptions.some((o) => o[idField] === valueState)
293
+ ? valueState
294
+ : ""
295
+ }
296
+ input={
297
+ <OutlinedInput notched label={label} required={inputRequired} />
298
+ }
299
+ labelId={labelId}
300
+ name={name}
301
+ multiple={multiple}
302
+ onChange={(event, child) => {
303
+ if (onChange) {
304
+ onChange(event, child);
305
+
306
+ // event.preventDefault() will block executing
307
+ if (event.defaultPrevented) return;
218
308
  }
219
- return diff;
220
- }
221
- return undefined;
222
- };
223
309
 
224
- // Get option id
225
- const getId = (option: T) => {
226
- return option[idField] as unknown as React.Key;
227
- };
228
-
229
- // Get option label
230
- const getLabel = (option: T) => {
231
- return typeof labelField === 'function'
232
- ? labelField(option)
233
- : (option[labelField] as string);
234
- };
310
+ // Set item value
311
+ const value = event.target.value;
312
+ if (multiple && !Array.isArray(value)) return;
235
313
 
236
- // Refs
237
- const divRef = React.useRef<HTMLDivElement>();
238
-
239
- // Refresh list data
240
- const refreshData = () => {
241
- if (loadData == null) return;
242
- loadData().then((result) => {
243
- if (result == null || !isMounted.current) return;
244
- if (onLoadData) onLoadData(result);
245
- if (autoAddBlankItem) {
246
- Utils.addBlankItem(result, idField, labelField);
314
+ const diff = setItemValue(value);
315
+ if (diff != null) {
316
+ doItemChange(localOptions, diff, true);
247
317
  }
248
- setOptionsAdd(result);
249
- });
250
- };
251
-
252
- // When value change
253
- React.useEffect(() => {
254
- refreshData();
255
- }, [valueSource]);
256
-
257
- // When layout ready
258
- React.useEffect(() => {
259
- const input = divRef.current?.querySelector('input');
260
- const inputChange = (event: Event) => {
261
- // Reset case
262
- if (event.cancelable) setValueState(multiple ? [] : '');
263
- };
264
- input?.addEventListener('change', inputChange);
265
-
266
- isMounted.current = true;
267
-
268
- return () => {
269
- isMounted.current = false;
270
- input?.removeEventListener('change', inputChange);
271
- };
272
- }, [multiple]);
273
-
274
- // Layout
275
- return (
276
- <Stack direction="row">
277
- <FormControl
278
- size={
279
- search ? MUGlobal.searchFieldSize : MUGlobal.inputFieldSize
280
- }
281
- fullWidth={fullWidth}
282
- error={error}
283
- >
284
- <InputLabel
285
- id={labelId}
286
- shrink={
287
- search
288
- ? MUGlobal.searchFieldShrink
289
- : MUGlobal.inputFieldShrink
290
- }
291
- >
292
- {label}
293
- </InputLabel>
294
- <Select
295
- ref={divRef}
296
- value={
297
- multiple
298
- ? valueState
299
- : localOptions.some(
300
- (o) => o[idField] === valueState
301
- )
302
- ? valueState
303
- : ''
318
+ }}
319
+ renderValue={(selected) => {
320
+ // The text shows up
321
+ return localOptions
322
+ .filter((option) => {
323
+ const id = getId(option);
324
+ return Array.isArray(selected)
325
+ ? selected.indexOf(id) !== -1
326
+ : selected === id;
327
+ })
328
+ .map((option) => getLabel(option))
329
+ .join(", ");
330
+ }}
331
+ sx={{ minWidth: "150px" }}
332
+ fullWidth={fullWidth}
333
+ {...rest}
334
+ >
335
+ {localOptions.map((option) => {
336
+ // Option id
337
+ const id = getId(option);
338
+
339
+ // Option label
340
+ const label = getLabel(option);
341
+
342
+ // Option
343
+ return (
344
+ <MenuItem
345
+ key={id}
346
+ value={id}
347
+ onClick={(event) => {
348
+ if (onItemClick) {
349
+ onItemClick(event, option);
350
+ }
351
+ }}
352
+ style={itemStyle == null ? undefined : itemStyle(option)}
353
+ >
354
+ {multiple && (
355
+ <Checkbox
356
+ checked={
357
+ Array.isArray(valueState)
358
+ ? valueState.includes(id)
359
+ : valueState === id
304
360
  }
305
- input={
306
- <OutlinedInput
307
- notched
308
- label={label}
309
- required={inputRequired}
310
- />
311
- }
312
- labelId={labelId}
313
- name={name}
314
- multiple={multiple}
315
- onChange={(event, child) => {
316
- if (onChange) {
317
- onChange(event, child);
318
-
319
- // event.preventDefault() will block executing
320
- if (event.defaultPrevented) return;
321
- }
322
-
323
- // Set item value
324
- const value = event.target.value;
325
- if (multiple && !Array.isArray(value)) return;
326
-
327
- const diff = setItemValue(value);
328
- if (diff != null) {
329
- doItemChange(localOptions, diff, true);
330
- }
331
- }}
332
- renderValue={(selected) => {
333
- // The text shows up
334
- return localOptions
335
- .filter((option) => {
336
- const id = getId(option);
337
- return Array.isArray(selected)
338
- ? selected.indexOf(id) !== -1
339
- : selected === id;
340
- })
341
- .map((option) => getLabel(option))
342
- .join(', ');
343
- }}
344
- sx={{ minWidth: '150px' }}
345
- fullWidth={fullWidth}
346
- {...rest}
347
- >
348
- {localOptions.map((option) => {
349
- // Option id
350
- const id = getId(option);
351
-
352
- // Option label
353
- const label = getLabel(option);
354
-
355
- // Option
356
- return (
357
- <MenuItem
358
- key={id}
359
- value={id}
360
- onClick={(event) => {
361
- if (onItemClick) {
362
- onItemClick(event, option);
363
- }
364
- }}
365
- style={
366
- itemStyle == null
367
- ? undefined
368
- : itemStyle(option)
369
- }
370
- >
371
- {multiple && (
372
- <Checkbox
373
- checked={
374
- Array.isArray(valueState)
375
- ? valueState.includes(id)
376
- : valueState === id
377
- }
378
- />
379
- )}
380
- <ListItemText primary={label} />
381
- {itemIconRenderer && (
382
- <ListItemRightIcon>
383
- {itemIconRenderer(option[idField])}
384
- </ListItemRightIcon>
385
- )}
386
- </MenuItem>
387
- );
388
- })}
389
- </Select>
390
- {helperText != null && (
391
- <FormHelperText>{helperText}</FormHelperText>
361
+ />
362
+ )}
363
+ <ListItemText primary={label} />
364
+ {itemIconRenderer && (
365
+ <ListItemRightIcon>
366
+ {itemIconRenderer(option[idField])}
367
+ </ListItemRightIcon>
392
368
  )}
393
- </FormControl>
394
- {refresh != null &&
395
- loadData != null &&
396
- (typeof refresh === 'string' ? (
397
- <IconButton
398
- size="small"
399
- title={refresh}
400
- onClick={refreshData}
401
- >
402
- <RefreshIcon />
403
- </IconButton>
404
- ) : (
405
- refresh
406
- ))}
407
- </Stack>
408
- );
369
+ </MenuItem>
370
+ );
371
+ })}
372
+ </Select>
373
+ {helperText != null && <FormHelperText>{helperText}</FormHelperText>}
374
+ </FormControl>
375
+ {refresh != null &&
376
+ loadData != null &&
377
+ (typeof refresh === "string" ? (
378
+ <IconButton size="small" title={refresh} onClick={refreshData}>
379
+ <RefreshIcon />
380
+ </IconButton>
381
+ ) : (
382
+ refresh
383
+ ))}
384
+ </Stack>
385
+ );
409
386
  }