@etsoo/materialui 1.2.52 → 1.2.54
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/ListChooser.d.ts +7 -3
- package/lib/ListChooser.js +25 -11
- package/lib/UserAvatarEditor.d.ts +5 -1
- package/lib/UserAvatarEditor.js +39 -7
- package/package.json +2 -2
- package/src/ListChooser.tsx +203 -176
- package/src/UserAvatarEditor.tsx +89 -16
package/lib/ListChooser.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DataTypes, DelayedExecutorType, IdDefaultType } from
|
|
2
|
-
import { ListItemButtonProps, ListProps } from
|
|
3
|
-
import React from
|
|
1
|
+
import { DataTypes, DelayedExecutorType, IdDefaultType } from "@etsoo/shared";
|
|
2
|
+
import { ListItemButtonProps, ListProps } from "@mui/material";
|
|
3
|
+
import React from "react";
|
|
4
4
|
type QueryData = {
|
|
5
5
|
title?: string;
|
|
6
6
|
};
|
|
@@ -46,6 +46,10 @@ export type ListChooserProps<T extends object, D extends DataTypes.Keys<T>, Q ex
|
|
|
46
46
|
* Title
|
|
47
47
|
*/
|
|
48
48
|
title: string;
|
|
49
|
+
/**
|
|
50
|
+
* Double click enabled
|
|
51
|
+
*/
|
|
52
|
+
doubleClickEnabled?: boolean;
|
|
49
53
|
};
|
|
50
54
|
/**
|
|
51
55
|
* List chooser
|
package/lib/ListChooser.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useDelayedExecutor } from
|
|
2
|
-
import { List, ListItem, ListItemButton, ListItemText, TextField } from
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import { useDelayedExecutor } from "@etsoo/react";
|
|
2
|
+
import { List, ListItem, ListItemButton, ListItemText, TextField } from "@mui/material";
|
|
3
|
+
import CheckBoxIcon from "@mui/icons-material/CheckBox";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { VBox } from "./FlexBox";
|
|
5
6
|
/**
|
|
6
7
|
* List chooser
|
|
7
8
|
* @param props Props
|
|
@@ -29,19 +30,20 @@ export function ListChooser(props) {
|
|
|
29
30
|
});
|
|
30
31
|
// Destruct
|
|
31
32
|
const { conditionRenderer = (rq, delayed) => (React.createElement(TextField, { autoFocus: true, margin: "dense", name: "title", label: title, fullWidth: true, variant: "standard", inputProps: { maxLength: 128 }, onChange: (event) => {
|
|
32
|
-
Reflect.set(rq,
|
|
33
|
+
Reflect.set(rq, "title", event.target.value);
|
|
33
34
|
delayed.call();
|
|
34
35
|
} })), itemRenderer = (item, selectProps) => {
|
|
35
36
|
const id = item[idField];
|
|
36
|
-
const
|
|
37
|
+
const sp = selectProps(id);
|
|
38
|
+
const label = typeof labelField === "function"
|
|
37
39
|
? labelField(item)
|
|
38
40
|
: Reflect.get(item, labelField);
|
|
39
|
-
return (React.createElement(ListItem, { disableGutters: true, key: `${id}
|
|
40
|
-
React.createElement(ListItemButton, { ...
|
|
41
|
+
return (React.createElement(ListItem, { disableGutters: true, key: `${id}`, secondaryAction: sp.selected ? React.createElement(CheckBoxIcon, { fontSize: "small" }) : undefined },
|
|
42
|
+
React.createElement(ListItemButton, { ...sp },
|
|
41
43
|
React.createElement(ListItemText, { primary: label }))));
|
|
42
|
-
}, idField =
|
|
44
|
+
}, idField = "id", labelField = "label", loadData, multiple = false, onItemChange, title, doubleClickEnabled = false, onDoubleClick, ...rest } = props;
|
|
43
45
|
// Default minimum height
|
|
44
|
-
(_a = rest.sx) !== null && _a !== void 0 ? _a : (rest.sx = { minHeight:
|
|
46
|
+
(_a = rest.sx) !== null && _a !== void 0 ? _a : (rest.sx = { minHeight: "220px" });
|
|
45
47
|
// State
|
|
46
48
|
const [items, setItems] = React.useState([]);
|
|
47
49
|
// Query request data
|
|
@@ -72,8 +74,20 @@ export function ListChooser(props) {
|
|
|
72
74
|
delayed.clear();
|
|
73
75
|
};
|
|
74
76
|
}, [delayed]);
|
|
77
|
+
const onDoubleClickLocal = (event) => {
|
|
78
|
+
var _a;
|
|
79
|
+
if (onDoubleClick)
|
|
80
|
+
onDoubleClick(event);
|
|
81
|
+
if (doubleClickEnabled) {
|
|
82
|
+
const button = (_a = event.currentTarget
|
|
83
|
+
.closest("form")) === null || _a === void 0 ? void 0 : _a.elements.namedItem("okButton");
|
|
84
|
+
if (button) {
|
|
85
|
+
button.click();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
75
89
|
// Layout
|
|
76
90
|
return (React.createElement(VBox, null,
|
|
77
91
|
conditionRenderer(rq.current, delayed),
|
|
78
|
-
React.createElement(List, { disablePadding: true, dense: true, ...rest }, items.map((item) => itemRenderer(item, selectProps)))));
|
|
92
|
+
React.createElement(List, { onDoubleClick: onDoubleClickLocal, disablePadding: true, dense: true, ...rest }, items.map((item) => itemRenderer(item, selectProps)))));
|
|
79
93
|
}
|
|
@@ -9,7 +9,7 @@ export interface UserAvatarEditorToBlob {
|
|
|
9
9
|
* User avatar editor on done handler
|
|
10
10
|
*/
|
|
11
11
|
export interface UserAvatarEditorOnDoneHandler {
|
|
12
|
-
(canvas: HTMLCanvasElement, toBlob: UserAvatarEditorToBlob): void;
|
|
12
|
+
(canvas: HTMLCanvasElement, toBlob: UserAvatarEditorToBlob, type: string): void;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* User avatar editor props
|
|
@@ -43,6 +43,10 @@ export interface UserAvatarEditorProps {
|
|
|
43
43
|
* Height of the editor
|
|
44
44
|
*/
|
|
45
45
|
height?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Value range
|
|
48
|
+
*/
|
|
49
|
+
range?: [number, number, number];
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* User avatar editor
|
package/lib/UserAvatarEditor.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Button, ButtonGroup, Skeleton, Slider, Stack } from "@mui/material";
|
|
1
|
+
import { Button, ButtonGroup, IconButton, Skeleton, Slider, Stack } from "@mui/material";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
|
4
4
|
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
|
5
5
|
import ClearAllIcon from "@mui/icons-material/ClearAll";
|
|
6
6
|
import ComputerIcon from "@mui/icons-material/Computer";
|
|
7
7
|
import DoneIcon from "@mui/icons-material/Done";
|
|
8
|
+
import RemoveIcon from "@mui/icons-material/Remove";
|
|
9
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
8
10
|
import { Labels } from "./app/Labels";
|
|
9
11
|
const defaultState = {
|
|
10
12
|
scale: 1,
|
|
@@ -18,7 +20,7 @@ const defaultState = {
|
|
|
18
20
|
*/
|
|
19
21
|
export function UserAvatarEditor(props) {
|
|
20
22
|
// Destruct
|
|
21
|
-
const { border = 30, image, maxWidth, onDone, scaledResult = false, width = 200, height = 200 } = props;
|
|
23
|
+
const { border = 30, image, maxWidth, onDone, scaledResult = false, width = 200, height = 200, range = [0.1, 2, 0.1] } = props;
|
|
22
24
|
// Container width
|
|
23
25
|
const containerWidth = width + 2 * border + 44 + 4;
|
|
24
26
|
// Calculated max width
|
|
@@ -27,6 +29,8 @@ export function UserAvatarEditor(props) {
|
|
|
27
29
|
const labels = Labels.UserAvatarEditor;
|
|
28
30
|
// Ref
|
|
29
31
|
const ref = React.createRef();
|
|
32
|
+
// Image type
|
|
33
|
+
const type = React.useRef("image/jpeg");
|
|
30
34
|
// Button ref
|
|
31
35
|
const buttonRef = React.createRef();
|
|
32
36
|
// Preview image state
|
|
@@ -35,12 +39,33 @@ export function UserAvatarEditor(props) {
|
|
|
35
39
|
const [ready, setReady] = React.useState(false);
|
|
36
40
|
// Editor states
|
|
37
41
|
const [editorState, setEditorState] = React.useState(defaultState);
|
|
42
|
+
// Range
|
|
43
|
+
const [min, max, step] = range;
|
|
44
|
+
const marks = [
|
|
45
|
+
{
|
|
46
|
+
value: min,
|
|
47
|
+
label: min.toString()
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: max,
|
|
51
|
+
label: max.toString()
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
if (min < 1) {
|
|
55
|
+
marks.splice(1, 0, { value: 1, label: "1" });
|
|
56
|
+
}
|
|
38
57
|
// Handle zoom
|
|
39
58
|
const handleZoom = (_event, value, _activeThumb) => {
|
|
40
59
|
const scale = typeof value === "number" ? value : value[0];
|
|
60
|
+
setScale(scale);
|
|
61
|
+
};
|
|
62
|
+
const setScale = (scale) => {
|
|
41
63
|
const newState = { ...editorState, scale };
|
|
42
64
|
setEditorState(newState);
|
|
43
65
|
};
|
|
66
|
+
const adjustScale = (isAdd) => {
|
|
67
|
+
setScale(editorState.scale + (isAdd ? step : -step));
|
|
68
|
+
};
|
|
44
69
|
// Handle image load
|
|
45
70
|
const handleLoad = () => {
|
|
46
71
|
setReady(true);
|
|
@@ -54,7 +79,9 @@ export function UserAvatarEditor(props) {
|
|
|
54
79
|
// Reset all settings
|
|
55
80
|
handleReset();
|
|
56
81
|
// Set new preview image
|
|
57
|
-
|
|
82
|
+
const file = files[0];
|
|
83
|
+
type.current = file.type;
|
|
84
|
+
setPreviewImage(file);
|
|
58
85
|
// Set ready state
|
|
59
86
|
setReady(false);
|
|
60
87
|
// Make the submit button visible
|
|
@@ -86,7 +113,7 @@ export function UserAvatarEditor(props) {
|
|
|
86
113
|
const picaInstance = pica();
|
|
87
114
|
// toBlob helper
|
|
88
115
|
// Convenience method, similar to canvas.toBlob(), but with promise interface & polyfill for old browsers.
|
|
89
|
-
const toBlob = (canvas, mimeType =
|
|
116
|
+
const toBlob = (canvas, mimeType = type.current, quality = 1) => {
|
|
90
117
|
return picaInstance.toBlob(canvas, mimeType, quality);
|
|
91
118
|
};
|
|
92
119
|
if (data.width > maxWidthCalculated) {
|
|
@@ -104,10 +131,10 @@ export function UserAvatarEditor(props) {
|
|
|
104
131
|
unsharpRadius: 0.6,
|
|
105
132
|
unsharpThreshold: 1
|
|
106
133
|
})
|
|
107
|
-
.then((result) => onDone(result, toBlob));
|
|
134
|
+
.then((result) => onDone(result, toBlob, type.current));
|
|
108
135
|
}
|
|
109
136
|
else {
|
|
110
|
-
onDone(data, toBlob);
|
|
137
|
+
onDone(data, toBlob, type.current);
|
|
111
138
|
}
|
|
112
139
|
};
|
|
113
140
|
// Load the component
|
|
@@ -126,6 +153,11 @@ export function UserAvatarEditor(props) {
|
|
|
126
153
|
React.createElement(RotateLeftIcon, null)),
|
|
127
154
|
React.createElement(Button, { onClick: handleReset, title: labels.reset },
|
|
128
155
|
React.createElement(ClearAllIcon, null)))),
|
|
129
|
-
React.createElement(
|
|
156
|
+
React.createElement(Stack, { spacing: 0.5, direction: "row", sx: { paddingBottom: 2 }, alignItems: "center" },
|
|
157
|
+
React.createElement(IconButton, { size: "small", disabled: !ready || editorState.scale <= min, onClick: () => adjustScale(false) },
|
|
158
|
+
React.createElement(RemoveIcon, null)),
|
|
159
|
+
React.createElement(Slider, { title: labels.zoom, disabled: !ready, min: min, max: max, step: step, value: editorState.scale, valueLabelDisplay: "auto", valueLabelFormat: (value) => `${Math.round(100 * value) / 100}`, marks: marks, onChange: handleZoom }),
|
|
160
|
+
React.createElement(IconButton, { size: "small", disabled: !ready || editorState.scale >= max, onClick: () => adjustScale(true) },
|
|
161
|
+
React.createElement(AddIcon, null))),
|
|
130
162
|
React.createElement(Button, { ref: buttonRef, variant: "contained", startIcon: React.createElement(DoneIcon, null), disabled: !ready, onClick: handleDone }, labels.done)));
|
|
131
163
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/materialui",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.54",
|
|
4
4
|
"description": "TypeScript Material-UI Implementation",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@mui/x-data-grid": "^6.7.0",
|
|
60
60
|
"@types/pica": "^9.0.1",
|
|
61
61
|
"@types/pulltorefreshjs": "^0.1.5",
|
|
62
|
-
"@types/react": "^18.2.
|
|
62
|
+
"@types/react": "^18.2.11",
|
|
63
63
|
"@types/react-avatar-editor": "^13.0.0",
|
|
64
64
|
"@types/react-dom": "^18.2.4",
|
|
65
65
|
"@types/react-input-mask": "^3.0.2",
|
package/src/ListChooser.tsx
CHANGED
|
@@ -1,84 +1,90 @@
|
|
|
1
|
-
import { useDelayedExecutor } from
|
|
2
|
-
import { DataTypes, DelayedExecutorType, IdDefaultType } from
|
|
1
|
+
import { useDelayedExecutor } from "@etsoo/react";
|
|
2
|
+
import { DataTypes, DelayedExecutorType, IdDefaultType } from "@etsoo/shared";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from
|
|
12
|
-
import
|
|
13
|
-
import
|
|
4
|
+
List,
|
|
5
|
+
ListItem,
|
|
6
|
+
ListItemButton,
|
|
7
|
+
ListItemButtonProps,
|
|
8
|
+
ListItemText,
|
|
9
|
+
ListProps,
|
|
10
|
+
TextField
|
|
11
|
+
} from "@mui/material";
|
|
12
|
+
import CheckBoxIcon from "@mui/icons-material/CheckBox";
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { VBox } from "./FlexBox";
|
|
14
15
|
|
|
15
16
|
type QueryData = {
|
|
16
|
-
|
|
17
|
+
title?: string;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* List chooser button props
|
|
21
22
|
*/
|
|
22
23
|
export interface ListChooserButtonProps<
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
T extends object,
|
|
25
|
+
D extends DataTypes.Keys<T>
|
|
25
26
|
> {
|
|
26
|
-
|
|
27
|
+
(id: T[D]): ListItemButtonProps;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* List chooser props
|
|
31
32
|
*/
|
|
32
33
|
export type ListChooserProps<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
T extends object,
|
|
35
|
+
D extends DataTypes.Keys<T>,
|
|
36
|
+
Q extends object
|
|
36
37
|
> = ListProps & {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Condition renderer
|
|
40
|
+
*/
|
|
41
|
+
conditionRenderer?: (
|
|
42
|
+
rq: Partial<Q>,
|
|
43
|
+
delayed: DelayedExecutorType
|
|
44
|
+
) => React.ReactNode;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* List item renderer
|
|
48
|
+
*/
|
|
49
|
+
itemRenderer?: (
|
|
50
|
+
data: T,
|
|
51
|
+
props: ListChooserButtonProps<T, D>
|
|
52
|
+
) => React.ReactNode;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Label field
|
|
56
|
+
*/
|
|
57
|
+
labelField?: DataTypes.Keys<T, string> | ((data: T) => string);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Id field
|
|
61
|
+
*/
|
|
62
|
+
idField?: D;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load data callback
|
|
66
|
+
*/
|
|
67
|
+
loadData: (rq: Partial<Q>) => PromiseLike<T[] | null | undefined>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Multiple selected
|
|
71
|
+
*/
|
|
72
|
+
multiple?: boolean;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Item onchange callback
|
|
76
|
+
*/
|
|
77
|
+
onItemChange: (items: T[], ids: T[D][]) => void;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Title
|
|
81
|
+
*/
|
|
82
|
+
title: string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Double click enabled
|
|
86
|
+
*/
|
|
87
|
+
doubleClickEnabled?: boolean;
|
|
82
88
|
};
|
|
83
89
|
|
|
84
90
|
/**
|
|
@@ -87,118 +93,139 @@ export type ListChooserProps<
|
|
|
87
93
|
* @returns Component
|
|
88
94
|
*/
|
|
89
95
|
export function ListChooser<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
T extends object,
|
|
97
|
+
D extends DataTypes.Keys<T> = IdDefaultType<T>,
|
|
98
|
+
Q extends object = QueryData
|
|
93
99
|
>(props: ListChooserProps<T, D, Q>) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
};
|
|
193
|
-
}, [delayed]);
|
|
194
|
-
|
|
195
|
-
// Layout
|
|
196
|
-
return (
|
|
197
|
-
<VBox>
|
|
198
|
-
{conditionRenderer(rq.current, delayed)}
|
|
199
|
-
<List disablePadding dense {...rest}>
|
|
200
|
-
{items.map((item) => itemRenderer(item, selectProps))}
|
|
201
|
-
</List>
|
|
202
|
-
</VBox>
|
|
100
|
+
// Selected ids state
|
|
101
|
+
const [selectedIds, setSelectedIds] = React.useState<T[D][]>([]);
|
|
102
|
+
|
|
103
|
+
const selectProps: ListChooserButtonProps<T, D> = (id: T[D]) => ({
|
|
104
|
+
selected: selectedIds.includes(id),
|
|
105
|
+
onClick: () => {
|
|
106
|
+
if (multiple) {
|
|
107
|
+
const index = selectedIds.indexOf(id);
|
|
108
|
+
if (index === -1) selectedIds.push(id);
|
|
109
|
+
else selectedIds.splice(index, 1);
|
|
110
|
+
setSelectedIds([...selectedIds]);
|
|
111
|
+
} else {
|
|
112
|
+
setSelectedIds([id]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Destruct
|
|
118
|
+
const {
|
|
119
|
+
conditionRenderer = (rq: Partial<Q>, delayed: DelayedExecutorType) => (
|
|
120
|
+
<TextField
|
|
121
|
+
autoFocus
|
|
122
|
+
margin="dense"
|
|
123
|
+
name="title"
|
|
124
|
+
label={title}
|
|
125
|
+
fullWidth
|
|
126
|
+
variant="standard"
|
|
127
|
+
inputProps={{ maxLength: 128 }}
|
|
128
|
+
onChange={(event) => {
|
|
129
|
+
Reflect.set(rq, "title", event.target.value);
|
|
130
|
+
delayed.call();
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
),
|
|
134
|
+
itemRenderer = (item, selectProps) => {
|
|
135
|
+
const id = item[idField];
|
|
136
|
+
const sp = selectProps(id);
|
|
137
|
+
const label =
|
|
138
|
+
typeof labelField === "function"
|
|
139
|
+
? labelField(item)
|
|
140
|
+
: (Reflect.get(item, labelField) as React.ReactNode);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<ListItem
|
|
144
|
+
disableGutters
|
|
145
|
+
key={`${id}`}
|
|
146
|
+
secondaryAction={
|
|
147
|
+
sp.selected ? <CheckBoxIcon fontSize="small" /> : undefined
|
|
148
|
+
}
|
|
149
|
+
>
|
|
150
|
+
<ListItemButton {...sp}>
|
|
151
|
+
<ListItemText primary={label} />
|
|
152
|
+
</ListItemButton>
|
|
153
|
+
</ListItem>
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
idField = "id" as D,
|
|
157
|
+
labelField = "label",
|
|
158
|
+
loadData,
|
|
159
|
+
multiple = false,
|
|
160
|
+
onItemChange,
|
|
161
|
+
title,
|
|
162
|
+
doubleClickEnabled = false,
|
|
163
|
+
onDoubleClick,
|
|
164
|
+
...rest
|
|
165
|
+
} = props;
|
|
166
|
+
|
|
167
|
+
// Default minimum height
|
|
168
|
+
rest.sx ??= { minHeight: "220px" };
|
|
169
|
+
|
|
170
|
+
// State
|
|
171
|
+
const [items, setItems] = React.useState<T[]>([]);
|
|
172
|
+
|
|
173
|
+
// Query request data
|
|
174
|
+
const mounted = React.useRef<boolean>(false);
|
|
175
|
+
const rq = React.useRef<Partial<Q>>({});
|
|
176
|
+
|
|
177
|
+
// Delayed execution
|
|
178
|
+
const delayed = useDelayedExecutor(async () => {
|
|
179
|
+
const result = await loadData(rq.current);
|
|
180
|
+
if (result == null || !mounted.current) return;
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
!multiple &&
|
|
184
|
+
selectedIds.length > 0 &&
|
|
185
|
+
!result.some((item) => selectedIds.includes(item[idField]))
|
|
186
|
+
) {
|
|
187
|
+
setSelectedIds([]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setItems(result);
|
|
191
|
+
}, 480);
|
|
192
|
+
|
|
193
|
+
React.useEffect(() => {
|
|
194
|
+
if (!mounted.current) return;
|
|
195
|
+
onItemChange(
|
|
196
|
+
items.filter((item) => selectedIds.includes(item[idField])),
|
|
197
|
+
selectedIds
|
|
203
198
|
);
|
|
199
|
+
}, [selectedIds]);
|
|
200
|
+
|
|
201
|
+
React.useEffect(() => {
|
|
202
|
+
mounted.current = true;
|
|
203
|
+
delayed.call(0);
|
|
204
|
+
return () => {
|
|
205
|
+
mounted.current = false;
|
|
206
|
+
delayed.clear();
|
|
207
|
+
};
|
|
208
|
+
}, [delayed]);
|
|
209
|
+
|
|
210
|
+
const onDoubleClickLocal = (event: React.MouseEvent<HTMLUListElement>) => {
|
|
211
|
+
if (onDoubleClick) onDoubleClick(event);
|
|
212
|
+
if (doubleClickEnabled) {
|
|
213
|
+
const button = event.currentTarget
|
|
214
|
+
.closest("form")
|
|
215
|
+
?.elements.namedItem("okButton") as HTMLButtonElement | undefined;
|
|
216
|
+
if (button) {
|
|
217
|
+
button.click();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Layout
|
|
223
|
+
return (
|
|
224
|
+
<VBox>
|
|
225
|
+
{conditionRenderer(rq.current, delayed)}
|
|
226
|
+
<List onDoubleClick={onDoubleClickLocal} disablePadding dense {...rest}>
|
|
227
|
+
{items.map((item) => itemRenderer(item, selectProps))}
|
|
228
|
+
</List>
|
|
229
|
+
</VBox>
|
|
230
|
+
);
|
|
204
231
|
}
|
package/src/UserAvatarEditor.tsx
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
ButtonGroup,
|
|
4
|
+
IconButton,
|
|
5
|
+
Skeleton,
|
|
6
|
+
Slider,
|
|
7
|
+
Stack
|
|
8
|
+
} from "@mui/material";
|
|
2
9
|
import React from "react";
|
|
3
10
|
import type AvatarEditor from "react-avatar-editor";
|
|
4
11
|
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
|
@@ -6,6 +13,8 @@ import RotateRightIcon from "@mui/icons-material/RotateRight";
|
|
|
6
13
|
import ClearAllIcon from "@mui/icons-material/ClearAll";
|
|
7
14
|
import ComputerIcon from "@mui/icons-material/Computer";
|
|
8
15
|
import DoneIcon from "@mui/icons-material/Done";
|
|
16
|
+
import RemoveIcon from "@mui/icons-material/Remove";
|
|
17
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
9
18
|
import { Labels } from "./app/Labels";
|
|
10
19
|
|
|
11
20
|
/**
|
|
@@ -23,7 +32,11 @@ export interface UserAvatarEditorToBlob {
|
|
|
23
32
|
* User avatar editor on done handler
|
|
24
33
|
*/
|
|
25
34
|
export interface UserAvatarEditorOnDoneHandler {
|
|
26
|
-
(
|
|
35
|
+
(
|
|
36
|
+
canvas: HTMLCanvasElement,
|
|
37
|
+
toBlob: UserAvatarEditorToBlob,
|
|
38
|
+
type: string
|
|
39
|
+
): void;
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
/**
|
|
@@ -64,6 +77,11 @@ export interface UserAvatarEditorProps {
|
|
|
64
77
|
* Height of the editor
|
|
65
78
|
*/
|
|
66
79
|
height?: number;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Value range
|
|
83
|
+
*/
|
|
84
|
+
range?: [number, number, number];
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
interface EditorState {
|
|
@@ -91,7 +109,8 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
91
109
|
onDone,
|
|
92
110
|
scaledResult = false,
|
|
93
111
|
width = 200,
|
|
94
|
-
height = 200
|
|
112
|
+
height = 200,
|
|
113
|
+
range = [0.1, 2, 0.1]
|
|
95
114
|
} = props;
|
|
96
115
|
|
|
97
116
|
// Container width
|
|
@@ -107,6 +126,9 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
107
126
|
// Ref
|
|
108
127
|
const ref = React.createRef<AvatarEditor>();
|
|
109
128
|
|
|
129
|
+
// Image type
|
|
130
|
+
const type = React.useRef<string>("image/jpeg");
|
|
131
|
+
|
|
110
132
|
// Button ref
|
|
111
133
|
const buttonRef = React.createRef<HTMLButtonElement>();
|
|
112
134
|
|
|
@@ -119,6 +141,23 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
119
141
|
// Editor states
|
|
120
142
|
const [editorState, setEditorState] = React.useState(defaultState);
|
|
121
143
|
|
|
144
|
+
// Range
|
|
145
|
+
const [min, max, step] = range;
|
|
146
|
+
const marks = [
|
|
147
|
+
{
|
|
148
|
+
value: min,
|
|
149
|
+
label: min.toString()
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
value: max,
|
|
153
|
+
label: max.toString()
|
|
154
|
+
}
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
if (min < 1) {
|
|
158
|
+
marks.splice(1, 0, { value: 1, label: "1" });
|
|
159
|
+
}
|
|
160
|
+
|
|
122
161
|
// Handle zoom
|
|
123
162
|
const handleZoom = (
|
|
124
163
|
_event: Event,
|
|
@@ -126,10 +165,18 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
126
165
|
_activeThumb: number
|
|
127
166
|
) => {
|
|
128
167
|
const scale = typeof value === "number" ? value : value[0];
|
|
168
|
+
setScale(scale);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const setScale = (scale: number) => {
|
|
129
172
|
const newState = { ...editorState, scale };
|
|
130
173
|
setEditorState(newState);
|
|
131
174
|
};
|
|
132
175
|
|
|
176
|
+
const adjustScale = (isAdd: boolean) => {
|
|
177
|
+
setScale(editorState.scale + (isAdd ? step : -step));
|
|
178
|
+
};
|
|
179
|
+
|
|
133
180
|
// Handle image load
|
|
134
181
|
const handleLoad = () => {
|
|
135
182
|
setReady(true);
|
|
@@ -144,7 +191,9 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
144
191
|
handleReset();
|
|
145
192
|
|
|
146
193
|
// Set new preview image
|
|
147
|
-
|
|
194
|
+
const file = files[0];
|
|
195
|
+
type.current = file.type;
|
|
196
|
+
setPreviewImage(file);
|
|
148
197
|
|
|
149
198
|
// Set ready state
|
|
150
199
|
setReady(false);
|
|
@@ -183,7 +232,7 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
183
232
|
// Convenience method, similar to canvas.toBlob(), but with promise interface & polyfill for old browsers.
|
|
184
233
|
const toBlob = (
|
|
185
234
|
canvas: HTMLCanvasElement,
|
|
186
|
-
mimeType: string =
|
|
235
|
+
mimeType: string = type.current,
|
|
187
236
|
quality: number = 1
|
|
188
237
|
) => {
|
|
189
238
|
return picaInstance.toBlob(canvas, mimeType, quality);
|
|
@@ -206,9 +255,9 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
206
255
|
unsharpRadius: 0.6,
|
|
207
256
|
unsharpThreshold: 1
|
|
208
257
|
})
|
|
209
|
-
.then((result) => onDone(result, toBlob));
|
|
258
|
+
.then((result) => onDone(result, toBlob, type.current));
|
|
210
259
|
} else {
|
|
211
|
-
onDone(data, toBlob);
|
|
260
|
+
onDone(data, toBlob, type.current);
|
|
212
261
|
}
|
|
213
262
|
};
|
|
214
263
|
|
|
@@ -263,15 +312,39 @@ export function UserAvatarEditor(props: UserAvatarEditorProps) {
|
|
|
263
312
|
</Button>
|
|
264
313
|
</ButtonGroup>
|
|
265
314
|
</Stack>
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
315
|
+
<Stack
|
|
316
|
+
spacing={0.5}
|
|
317
|
+
direction="row"
|
|
318
|
+
sx={{ paddingBottom: 2 }}
|
|
319
|
+
alignItems="center"
|
|
320
|
+
>
|
|
321
|
+
<IconButton
|
|
322
|
+
size="small"
|
|
323
|
+
disabled={!ready || editorState.scale <= min}
|
|
324
|
+
onClick={() => adjustScale(false)}
|
|
325
|
+
>
|
|
326
|
+
<RemoveIcon />
|
|
327
|
+
</IconButton>
|
|
328
|
+
<Slider
|
|
329
|
+
title={labels.zoom}
|
|
330
|
+
disabled={!ready}
|
|
331
|
+
min={min}
|
|
332
|
+
max={max}
|
|
333
|
+
step={step}
|
|
334
|
+
value={editorState.scale}
|
|
335
|
+
valueLabelDisplay="auto"
|
|
336
|
+
valueLabelFormat={(value) => `${Math.round(100 * value) / 100}`}
|
|
337
|
+
marks={marks}
|
|
338
|
+
onChange={handleZoom}
|
|
339
|
+
/>
|
|
340
|
+
<IconButton
|
|
341
|
+
size="small"
|
|
342
|
+
disabled={!ready || editorState.scale >= max}
|
|
343
|
+
onClick={() => adjustScale(true)}
|
|
344
|
+
>
|
|
345
|
+
<AddIcon />
|
|
346
|
+
</IconButton>
|
|
347
|
+
</Stack>
|
|
275
348
|
<Button
|
|
276
349
|
ref={buttonRef}
|
|
277
350
|
variant="contained"
|