@etsoo/materialui 1.0.28 → 1.0.29
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/SelectEx.d.ts +4 -0
- package/lib/SelectEx.js +67 -57
- package/package.json +8 -8
- package/src/SelectEx.tsx +137 -103
package/lib/SelectEx.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export declare type SelectExProps<T extends object, D extends DataTypes.Keys<T>
|
|
|
53
53
|
* Array of options.
|
|
54
54
|
*/
|
|
55
55
|
options?: ReadonlyArray<T>;
|
|
56
|
+
/**
|
|
57
|
+
* Supports refresh label or component
|
|
58
|
+
*/
|
|
59
|
+
refresh?: string | React.ReactNode;
|
|
56
60
|
/**
|
|
57
61
|
* Is search case?
|
|
58
62
|
*/
|
package/lib/SelectEx.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Checkbox, FormControl, FormHelperText, InputLabel, ListItemText, MenuItem, OutlinedInput, Select } from '@mui/material';
|
|
1
|
+
import { Checkbox, FormControl, FormHelperText, IconButton, InputLabel, ListItemText, MenuItem, OutlinedInput, Select, Stack } from '@mui/material';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { MUGlobal } from './MUGlobal';
|
|
4
4
|
import { ListItemRightIcon } from './ListItemRightIcon';
|
|
5
|
+
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
5
6
|
import { Utils } from '@etsoo/shared';
|
|
6
7
|
/**
|
|
7
8
|
* Extended select component
|
|
@@ -11,7 +12,7 @@ import { Utils } from '@etsoo/shared';
|
|
|
11
12
|
export function SelectEx(props) {
|
|
12
13
|
var _a;
|
|
13
14
|
// Destruct
|
|
14
|
-
const { defaultValue, idField = 'id', error, helperText, inputRequired, itemIconRenderer, itemStyle, label, labelField = 'label', loadData, onItemChange, onItemClick, onLoadData, multiple = false, name, options, search = false, autoAddBlankItem = search, value, onChange, fullWidth, ...rest } = props;
|
|
15
|
+
const { defaultValue, idField = 'id', error, helperText, inputRequired, itemIconRenderer, itemStyle, label, labelField = 'label', loadData, onItemChange, onItemClick, onLoadData, multiple = false, name, options, refresh, search = false, autoAddBlankItem = search, value, onChange, fullWidth, ...rest } = props;
|
|
15
16
|
// Options state
|
|
16
17
|
const [localOptions, setOptions] = React.useState([]);
|
|
17
18
|
const isMounted = React.useRef(true);
|
|
@@ -97,20 +98,24 @@ export function SelectEx(props) {
|
|
|
97
98
|
};
|
|
98
99
|
// Refs
|
|
99
100
|
const divRef = React.useRef();
|
|
101
|
+
// Refresh list data
|
|
102
|
+
const refreshData = () => {
|
|
103
|
+
if (loadData == null)
|
|
104
|
+
return;
|
|
105
|
+
loadData().then((result) => {
|
|
106
|
+
if (result == null || !isMounted.current)
|
|
107
|
+
return;
|
|
108
|
+
if (onLoadData)
|
|
109
|
+
onLoadData(result);
|
|
110
|
+
if (autoAddBlankItem) {
|
|
111
|
+
Utils.addBlankItem(result, idField, labelField);
|
|
112
|
+
}
|
|
113
|
+
setOptionsAdd(result);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
100
116
|
// When value change
|
|
101
117
|
React.useEffect(() => {
|
|
102
|
-
|
|
103
|
-
loadData().then((result) => {
|
|
104
|
-
if (result == null || !isMounted.current)
|
|
105
|
-
return;
|
|
106
|
-
if (onLoadData)
|
|
107
|
-
onLoadData(result);
|
|
108
|
-
if (autoAddBlankItem) {
|
|
109
|
-
Utils.addBlankItem(result, idField, labelField);
|
|
110
|
-
}
|
|
111
|
-
setOptionsAdd(result);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
118
|
+
refreshData();
|
|
114
119
|
}, [localValue]);
|
|
115
120
|
// When layout ready
|
|
116
121
|
React.useEffect(() => {
|
|
@@ -128,48 +133,53 @@ export function SelectEx(props) {
|
|
|
128
133
|
};
|
|
129
134
|
}, []);
|
|
130
135
|
// Layout
|
|
131
|
-
return (React.createElement(
|
|
132
|
-
React.createElement(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
onChange
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
doItemChange(localOptions, event.target.value, true);
|
|
145
|
-
handleChange(event);
|
|
146
|
-
}, renderValue: (selected) => {
|
|
147
|
-
// The text shows up
|
|
148
|
-
return localOptions
|
|
149
|
-
.filter((option) => {
|
|
150
|
-
const id = getId(option);
|
|
151
|
-
return Array.isArray(selected)
|
|
152
|
-
? selected.indexOf(id) !== -1
|
|
153
|
-
: selected === id;
|
|
154
|
-
})
|
|
155
|
-
.map((option) => getLabel(option))
|
|
156
|
-
.join(', ');
|
|
157
|
-
}, sx: { minWidth: '150px' }, fullWidth: fullWidth, ...rest }, localOptions.map((option) => {
|
|
158
|
-
// Option id
|
|
159
|
-
const id = getId(option);
|
|
160
|
-
// Option label
|
|
161
|
-
const label = getLabel(option);
|
|
162
|
-
// Option
|
|
163
|
-
return (React.createElement(MenuItem, { key: id, value: id, onClick: (event) => {
|
|
164
|
-
if (onItemClick) {
|
|
165
|
-
onItemClick(event, option);
|
|
136
|
+
return (React.createElement(Stack, { direction: "row" },
|
|
137
|
+
React.createElement(FormControl, { size: search ? MUGlobal.searchFieldSize : MUGlobal.inputFieldSize, fullWidth: fullWidth, error: error },
|
|
138
|
+
React.createElement(InputLabel, { id: labelId, shrink: search
|
|
139
|
+
? MUGlobal.searchFieldShrink
|
|
140
|
+
: MUGlobal.inputFieldShrink }, label),
|
|
141
|
+
React.createElement(Select, { ref: divRef, value: localOptions.some((option) => itemChecked(getId(option)))
|
|
142
|
+
? valueState !== null && valueState !== void 0 ? valueState : ''
|
|
143
|
+
: '', input: React.createElement(OutlinedInput, { notched: true, label: label, required: inputRequired }), labelId: labelId, name: name, multiple: multiple, onChange: (event, child) => {
|
|
144
|
+
if (onChange) {
|
|
145
|
+
onChange(event, child);
|
|
146
|
+
// event.preventDefault() will block executing
|
|
147
|
+
if (event.defaultPrevented)
|
|
148
|
+
return;
|
|
166
149
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
150
|
+
doItemChange(localOptions, event.target.value, true);
|
|
151
|
+
handleChange(event);
|
|
152
|
+
}, renderValue: (selected) => {
|
|
153
|
+
// The text shows up
|
|
154
|
+
return localOptions
|
|
155
|
+
.filter((option) => {
|
|
156
|
+
const id = getId(option);
|
|
157
|
+
return Array.isArray(selected)
|
|
158
|
+
? selected.indexOf(id) !== -1
|
|
159
|
+
: selected === id;
|
|
160
|
+
})
|
|
161
|
+
.map((option) => getLabel(option))
|
|
162
|
+
.join(', ');
|
|
163
|
+
}, sx: { minWidth: '150px' }, fullWidth: fullWidth, ...rest }, localOptions.map((option) => {
|
|
164
|
+
// Option id
|
|
165
|
+
const id = getId(option);
|
|
166
|
+
// Option label
|
|
167
|
+
const label = getLabel(option);
|
|
168
|
+
// Option
|
|
169
|
+
return (React.createElement(MenuItem, { key: id, value: id, onClick: (event) => {
|
|
170
|
+
if (onItemClick) {
|
|
171
|
+
onItemClick(event, option);
|
|
172
|
+
}
|
|
173
|
+
}, style: itemStyle == null
|
|
174
|
+
? undefined
|
|
175
|
+
: itemStyle(option) },
|
|
176
|
+
multiple && (React.createElement(Checkbox, { checked: itemChecked(id) })),
|
|
177
|
+
React.createElement(ListItemText, { primary: label }),
|
|
178
|
+
itemIconRenderer && (React.createElement(ListItemRightIcon, null, itemIconRenderer(option[idField])))));
|
|
179
|
+
})),
|
|
180
|
+
helperText != null && (React.createElement(FormHelperText, null, helperText))),
|
|
181
|
+
refresh != null &&
|
|
182
|
+
loadData != null &&
|
|
183
|
+
(typeof refresh === 'string' ? (React.createElement(IconButton, { size: "small", title: refresh, onClick: refreshData },
|
|
184
|
+
React.createElement(RefreshIcon, null))) : (refresh))));
|
|
175
185
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/materialui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "TypeScript Material-UI Implementation",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"@emotion/css": "^11.10.0",
|
|
52
52
|
"@emotion/react": "^11.10.4",
|
|
53
53
|
"@emotion/styled": "^11.10.4",
|
|
54
|
-
"@etsoo/appscript": "^1.2.
|
|
54
|
+
"@etsoo/appscript": "^1.2.97",
|
|
55
55
|
"@etsoo/notificationbase": "^1.1.7",
|
|
56
|
-
"@etsoo/react": "^1.6.
|
|
57
|
-
"@etsoo/shared": "^1.1.
|
|
56
|
+
"@etsoo/react": "^1.6.3",
|
|
57
|
+
"@etsoo/shared": "^1.1.58",
|
|
58
58
|
"@mui/icons-material": "^5.10.6",
|
|
59
59
|
"@mui/material": "^5.10.6",
|
|
60
60
|
"@types/pica": "^9.0.1",
|
|
61
61
|
"@types/pulltorefreshjs": "^0.1.5",
|
|
62
|
-
"@types/react": "^18.0.
|
|
62
|
+
"@types/react": "^18.0.21",
|
|
63
63
|
"@types/react-avatar-editor": "^13.0.0",
|
|
64
64
|
"@types/react-dom": "^18.0.6",
|
|
65
65
|
"@types/react-input-mask": "^3.0.1",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"react-dom": "^18.2.0",
|
|
72
72
|
"react-draggable": "^4.4.5",
|
|
73
73
|
"react-imask": "^6.4.3",
|
|
74
|
-
"react-router-dom": "^6.4.
|
|
74
|
+
"react-router-dom": "^6.4.1",
|
|
75
75
|
"react-window": "^1.8.7"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
@@ -85,13 +85,13 @@
|
|
|
85
85
|
"@types/jest": "^29.0.3",
|
|
86
86
|
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
|
87
87
|
"@typescript-eslint/parser": "^5.38.0",
|
|
88
|
-
"eslint": "^8.
|
|
88
|
+
"eslint": "^8.24.0",
|
|
89
89
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
90
90
|
"eslint-plugin-import": "^2.26.0",
|
|
91
91
|
"eslint-plugin-react": "^7.31.8",
|
|
92
92
|
"jest": "^29.0.3",
|
|
93
93
|
"jest-environment-jsdom": "^29.0.3",
|
|
94
|
-
"ts-jest": "^29.0.
|
|
94
|
+
"ts-jest": "^29.0.2",
|
|
95
95
|
"typescript": "^4.8.3"
|
|
96
96
|
}
|
|
97
97
|
}
|
package/src/SelectEx.tsx
CHANGED
|
@@ -2,17 +2,20 @@ import {
|
|
|
2
2
|
Checkbox,
|
|
3
3
|
FormControl,
|
|
4
4
|
FormHelperText,
|
|
5
|
+
IconButton,
|
|
5
6
|
InputLabel,
|
|
6
7
|
ListItemText,
|
|
7
8
|
MenuItem,
|
|
8
9
|
OutlinedInput,
|
|
9
10
|
Select,
|
|
10
11
|
SelectChangeEvent,
|
|
11
|
-
SelectProps
|
|
12
|
+
SelectProps,
|
|
13
|
+
Stack
|
|
12
14
|
} from '@mui/material';
|
|
13
15
|
import React from 'react';
|
|
14
16
|
import { MUGlobal } from './MUGlobal';
|
|
15
17
|
import { ListItemRightIcon } from './ListItemRightIcon';
|
|
18
|
+
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
16
19
|
import {
|
|
17
20
|
DataTypes,
|
|
18
21
|
IdDefaultType,
|
|
@@ -89,6 +92,11 @@ export type SelectExProps<
|
|
|
89
92
|
*/
|
|
90
93
|
options?: ReadonlyArray<T>;
|
|
91
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Supports refresh label or component
|
|
97
|
+
*/
|
|
98
|
+
refresh?: string | React.ReactNode;
|
|
99
|
+
|
|
92
100
|
/**
|
|
93
101
|
* Is search case?
|
|
94
102
|
*/
|
|
@@ -123,6 +131,7 @@ export function SelectEx<
|
|
|
123
131
|
multiple = false,
|
|
124
132
|
name,
|
|
125
133
|
options,
|
|
134
|
+
refresh,
|
|
126
135
|
search = false,
|
|
127
136
|
autoAddBlankItem = search,
|
|
128
137
|
value,
|
|
@@ -226,18 +235,22 @@ export function SelectEx<
|
|
|
226
235
|
// Refs
|
|
227
236
|
const divRef = React.useRef<HTMLDivElement>();
|
|
228
237
|
|
|
238
|
+
// Refresh list data
|
|
239
|
+
const refreshData = () => {
|
|
240
|
+
if (loadData == null) return;
|
|
241
|
+
loadData().then((result) => {
|
|
242
|
+
if (result == null || !isMounted.current) return;
|
|
243
|
+
if (onLoadData) onLoadData(result);
|
|
244
|
+
if (autoAddBlankItem) {
|
|
245
|
+
Utils.addBlankItem(result, idField, labelField);
|
|
246
|
+
}
|
|
247
|
+
setOptionsAdd(result);
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
229
251
|
// When value change
|
|
230
252
|
React.useEffect(() => {
|
|
231
|
-
|
|
232
|
-
loadData().then((result) => {
|
|
233
|
-
if (result == null || !isMounted.current) return;
|
|
234
|
-
if (onLoadData) onLoadData(result);
|
|
235
|
-
if (autoAddBlankItem) {
|
|
236
|
-
Utils.addBlankItem(result, idField, labelField);
|
|
237
|
-
}
|
|
238
|
-
setOptionsAdd(result);
|
|
239
|
-
});
|
|
240
|
-
}
|
|
253
|
+
refreshData();
|
|
241
254
|
}, [localValue]);
|
|
242
255
|
|
|
243
256
|
// When layout ready
|
|
@@ -257,101 +270,122 @@ export function SelectEx<
|
|
|
257
270
|
|
|
258
271
|
// Layout
|
|
259
272
|
return (
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
>
|
|
265
|
-
<InputLabel
|
|
266
|
-
id={labelId}
|
|
267
|
-
shrink={
|
|
268
|
-
search
|
|
269
|
-
? MUGlobal.searchFieldShrink
|
|
270
|
-
: MUGlobal.inputFieldShrink
|
|
273
|
+
<Stack direction="row">
|
|
274
|
+
<FormControl
|
|
275
|
+
size={
|
|
276
|
+
search ? MUGlobal.searchFieldSize : MUGlobal.inputFieldSize
|
|
271
277
|
}
|
|
272
|
-
>
|
|
273
|
-
{label}
|
|
274
|
-
</InputLabel>
|
|
275
|
-
<Select
|
|
276
|
-
ref={divRef}
|
|
277
|
-
value={
|
|
278
|
-
localOptions.some((option) => itemChecked(getId(option)))
|
|
279
|
-
? valueState ?? ''
|
|
280
|
-
: ''
|
|
281
|
-
}
|
|
282
|
-
input={
|
|
283
|
-
<OutlinedInput
|
|
284
|
-
notched
|
|
285
|
-
label={label}
|
|
286
|
-
required={inputRequired}
|
|
287
|
-
/>
|
|
288
|
-
}
|
|
289
|
-
labelId={labelId}
|
|
290
|
-
name={name}
|
|
291
|
-
multiple={multiple}
|
|
292
|
-
onChange={(event, child) => {
|
|
293
|
-
if (onChange) {
|
|
294
|
-
onChange(event, child);
|
|
295
|
-
|
|
296
|
-
// event.preventDefault() will block executing
|
|
297
|
-
if (event.defaultPrevented) return;
|
|
298
|
-
}
|
|
299
|
-
doItemChange(localOptions, event.target.value, true);
|
|
300
|
-
handleChange(event);
|
|
301
|
-
}}
|
|
302
|
-
renderValue={(selected) => {
|
|
303
|
-
// The text shows up
|
|
304
|
-
return localOptions
|
|
305
|
-
.filter((option) => {
|
|
306
|
-
const id = getId(option);
|
|
307
|
-
return Array.isArray(selected)
|
|
308
|
-
? selected.indexOf(id) !== -1
|
|
309
|
-
: selected === id;
|
|
310
|
-
})
|
|
311
|
-
.map((option) => getLabel(option))
|
|
312
|
-
.join(', ');
|
|
313
|
-
}}
|
|
314
|
-
sx={{ minWidth: '150px' }}
|
|
315
278
|
fullWidth={fullWidth}
|
|
316
|
-
{
|
|
279
|
+
error={error}
|
|
317
280
|
>
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
281
|
+
<InputLabel
|
|
282
|
+
id={labelId}
|
|
283
|
+
shrink={
|
|
284
|
+
search
|
|
285
|
+
? MUGlobal.searchFieldShrink
|
|
286
|
+
: MUGlobal.inputFieldShrink
|
|
287
|
+
}
|
|
288
|
+
>
|
|
289
|
+
{label}
|
|
290
|
+
</InputLabel>
|
|
291
|
+
<Select
|
|
292
|
+
ref={divRef}
|
|
293
|
+
value={
|
|
294
|
+
localOptions.some((option) =>
|
|
295
|
+
itemChecked(getId(option))
|
|
296
|
+
)
|
|
297
|
+
? valueState ?? ''
|
|
298
|
+
: ''
|
|
299
|
+
}
|
|
300
|
+
input={
|
|
301
|
+
<OutlinedInput
|
|
302
|
+
notched
|
|
303
|
+
label={label}
|
|
304
|
+
required={inputRequired}
|
|
305
|
+
/>
|
|
306
|
+
}
|
|
307
|
+
labelId={labelId}
|
|
308
|
+
name={name}
|
|
309
|
+
multiple={multiple}
|
|
310
|
+
onChange={(event, child) => {
|
|
311
|
+
if (onChange) {
|
|
312
|
+
onChange(event, child);
|
|
313
|
+
|
|
314
|
+
// event.preventDefault() will block executing
|
|
315
|
+
if (event.defaultPrevented) return;
|
|
316
|
+
}
|
|
317
|
+
doItemChange(localOptions, event.target.value, true);
|
|
318
|
+
handleChange(event);
|
|
319
|
+
}}
|
|
320
|
+
renderValue={(selected) => {
|
|
321
|
+
// The text shows up
|
|
322
|
+
return localOptions
|
|
323
|
+
.filter((option) => {
|
|
324
|
+
const id = getId(option);
|
|
325
|
+
return Array.isArray(selected)
|
|
326
|
+
? selected.indexOf(id) !== -1
|
|
327
|
+
: selected === id;
|
|
328
|
+
})
|
|
329
|
+
.map((option) => getLabel(option))
|
|
330
|
+
.join(', ');
|
|
331
|
+
}}
|
|
332
|
+
sx={{ minWidth: '150px' }}
|
|
333
|
+
fullWidth={fullWidth}
|
|
334
|
+
{...rest}
|
|
335
|
+
>
|
|
336
|
+
{localOptions.map((option) => {
|
|
337
|
+
// Option id
|
|
338
|
+
const id = getId(option);
|
|
339
|
+
|
|
340
|
+
// Option label
|
|
341
|
+
const label = getLabel(option);
|
|
342
|
+
|
|
343
|
+
// Option
|
|
344
|
+
return (
|
|
345
|
+
<MenuItem
|
|
346
|
+
key={id}
|
|
347
|
+
value={id}
|
|
348
|
+
onClick={(event) => {
|
|
349
|
+
if (onItemClick) {
|
|
350
|
+
onItemClick(event, option);
|
|
351
|
+
}
|
|
352
|
+
}}
|
|
353
|
+
style={
|
|
354
|
+
itemStyle == null
|
|
355
|
+
? undefined
|
|
356
|
+
: itemStyle(option)
|
|
333
357
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
</
|
|
352
|
-
{
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
358
|
+
>
|
|
359
|
+
{multiple && (
|
|
360
|
+
<Checkbox checked={itemChecked(id)} />
|
|
361
|
+
)}
|
|
362
|
+
<ListItemText primary={label} />
|
|
363
|
+
{itemIconRenderer && (
|
|
364
|
+
<ListItemRightIcon>
|
|
365
|
+
{itemIconRenderer(option[idField])}
|
|
366
|
+
</ListItemRightIcon>
|
|
367
|
+
)}
|
|
368
|
+
</MenuItem>
|
|
369
|
+
);
|
|
370
|
+
})}
|
|
371
|
+
</Select>
|
|
372
|
+
{helperText != null && (
|
|
373
|
+
<FormHelperText>{helperText}</FormHelperText>
|
|
374
|
+
)}
|
|
375
|
+
</FormControl>
|
|
376
|
+
{refresh != null &&
|
|
377
|
+
loadData != null &&
|
|
378
|
+
(typeof refresh === 'string' ? (
|
|
379
|
+
<IconButton
|
|
380
|
+
size="small"
|
|
381
|
+
title={refresh}
|
|
382
|
+
onClick={refreshData}
|
|
383
|
+
>
|
|
384
|
+
<RefreshIcon />
|
|
385
|
+
</IconButton>
|
|
386
|
+
) : (
|
|
387
|
+
refresh
|
|
388
|
+
))}
|
|
389
|
+
</Stack>
|
|
356
390
|
);
|
|
357
391
|
}
|