@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.
@@ -1,6 +1,6 @@
1
- import { DataTypes, DelayedExecutorType, IdDefaultType } from '@etsoo/shared';
2
- import { ListItemButtonProps, ListProps } from '@mui/material';
3
- import React from 'react';
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
@@ -1,7 +1,8 @@
1
- import { useDelayedExecutor } from '@etsoo/react';
2
- import { List, ListItem, ListItemButton, ListItemText, TextField } from '@mui/material';
3
- import React from 'react';
4
- import { VBox } from './FlexBox';
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, 'title', event.target.value);
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 label = typeof labelField === 'function'
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, { ...selectProps(id) },
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 = 'id', labelField = 'label', loadData, multiple = false, onItemChange, title, ...rest } = props;
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: '220px' });
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
@@ -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
- setPreviewImage(files[0]);
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 = "image/jpeg", quality = 1) => {
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(Slider, { title: labels.zoom, disabled: !ready, min: 1, max: 5, step: 0.01, value: editorState.scale, onChange: handleZoom }),
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.52",
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.9",
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",
@@ -1,84 +1,90 @@
1
- import { useDelayedExecutor } from '@etsoo/react';
2
- import { DataTypes, DelayedExecutorType, IdDefaultType } from '@etsoo/shared';
1
+ import { useDelayedExecutor } from "@etsoo/react";
2
+ import { DataTypes, DelayedExecutorType, IdDefaultType } from "@etsoo/shared";
3
3
  import {
4
- List,
5
- ListItem,
6
- ListItemButton,
7
- ListItemButtonProps,
8
- ListItemText,
9
- ListProps,
10
- TextField
11
- } from '@mui/material';
12
- import React from 'react';
13
- import { VBox } from './FlexBox';
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
- title?: string;
17
+ title?: string;
17
18
  };
18
19
 
19
20
  /**
20
21
  * List chooser button props
21
22
  */
22
23
  export interface ListChooserButtonProps<
23
- T extends object,
24
- D extends DataTypes.Keys<T>
24
+ T extends object,
25
+ D extends DataTypes.Keys<T>
25
26
  > {
26
- (id: T[D]): ListItemButtonProps;
27
+ (id: T[D]): ListItemButtonProps;
27
28
  }
28
29
 
29
30
  /**
30
31
  * List chooser props
31
32
  */
32
33
  export type ListChooserProps<
33
- T extends object,
34
- D extends DataTypes.Keys<T>,
35
- Q extends object
34
+ T extends object,
35
+ D extends DataTypes.Keys<T>,
36
+ Q extends object
36
37
  > = ListProps & {
37
- /**
38
- * Condition renderer
39
- */
40
- conditionRenderer?: (
41
- rq: Partial<Q>,
42
- delayed: DelayedExecutorType
43
- ) => React.ReactNode;
44
-
45
- /**
46
- * List item renderer
47
- */
48
- itemRenderer?: (
49
- data: T,
50
- props: ListChooserButtonProps<T, D>
51
- ) => React.ReactNode;
52
-
53
- /**
54
- * Label field
55
- */
56
- labelField?: DataTypes.Keys<T, string> | ((data: T) => string);
57
-
58
- /**
59
- * Id field
60
- */
61
- idField?: D;
62
-
63
- /**
64
- * Load data callback
65
- */
66
- loadData: (rq: Partial<Q>) => PromiseLike<T[] | null | undefined>;
67
-
68
- /**
69
- * Multiple selected
70
- */
71
- multiple?: boolean;
72
-
73
- /**
74
- * Item onchange callback
75
- */
76
- onItemChange: (items: T[], ids: T[D][]) => void;
77
-
78
- /**
79
- * Title
80
- */
81
- title: string;
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
- T extends object,
91
- D extends DataTypes.Keys<T> = IdDefaultType<T>,
92
- Q extends object = QueryData
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
- // Selected ids state
95
- const [selectedIds, setSelectedIds] = React.useState<T[D][]>([]);
96
-
97
- const selectProps: ListChooserButtonProps<T, D> = (id: T[D]) => ({
98
- selected: selectedIds.includes(id),
99
- onClick: () => {
100
- if (multiple) {
101
- const index = selectedIds.indexOf(id);
102
- if (index === -1) selectedIds.push(id);
103
- else selectedIds.splice(index, 1);
104
- setSelectedIds([...selectedIds]);
105
- } else {
106
- setSelectedIds([id]);
107
- }
108
- }
109
- });
110
-
111
- // Destruct
112
- const {
113
- conditionRenderer = (rq: Partial<Q>, delayed: DelayedExecutorType) => (
114
- <TextField
115
- autoFocus
116
- margin="dense"
117
- name="title"
118
- label={title}
119
- fullWidth
120
- variant="standard"
121
- inputProps={{ maxLength: 128 }}
122
- onChange={(event) => {
123
- Reflect.set(rq, 'title', event.target.value);
124
- delayed.call();
125
- }}
126
- />
127
- ),
128
- itemRenderer = (item, selectProps) => {
129
- const id = item[idField];
130
- const label =
131
- typeof labelField === 'function'
132
- ? labelField(item)
133
- : (Reflect.get(item, labelField) as React.ReactNode);
134
-
135
- return (
136
- <ListItem disableGutters key={`${id}`}>
137
- <ListItemButton {...selectProps(id)}>
138
- <ListItemText primary={label} />
139
- </ListItemButton>
140
- </ListItem>
141
- );
142
- },
143
- idField = 'id' as D,
144
- labelField = 'label',
145
- loadData,
146
- multiple = false,
147
- onItemChange,
148
- title,
149
- ...rest
150
- } = props;
151
-
152
- // Default minimum height
153
- rest.sx ??= { minHeight: '220px' };
154
-
155
- // State
156
- const [items, setItems] = React.useState<T[]>([]);
157
-
158
- // Query request data
159
- const mounted = React.useRef<boolean>(false);
160
- const rq = React.useRef<Partial<Q>>({});
161
-
162
- // Delayed execution
163
- const delayed = useDelayedExecutor(async () => {
164
- const result = await loadData(rq.current);
165
- if (result == null || !mounted.current) return;
166
-
167
- if (
168
- !multiple &&
169
- selectedIds.length > 0 &&
170
- !result.some((item) => selectedIds.includes(item[idField]))
171
- ) {
172
- setSelectedIds([]);
173
- }
174
-
175
- setItems(result);
176
- }, 480);
177
-
178
- React.useEffect(() => {
179
- if (!mounted.current) return;
180
- onItemChange(
181
- items.filter((item) => selectedIds.includes(item[idField])),
182
- selectedIds
183
- );
184
- }, [selectedIds]);
185
-
186
- React.useEffect(() => {
187
- mounted.current = true;
188
- delayed.call(0);
189
- return () => {
190
- mounted.current = false;
191
- delayed.clear();
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
  }
@@ -1,4 +1,11 @@
1
- import { Button, ButtonGroup, Skeleton, Slider, Stack } from "@mui/material";
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
- (canvas: HTMLCanvasElement, toBlob: UserAvatarEditorToBlob): void;
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
- setPreviewImage(files[0]);
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 = "image/jpeg",
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
- <Slider
267
- title={labels.zoom}
268
- disabled={!ready}
269
- min={1}
270
- max={5}
271
- step={0.01}
272
- value={editorState.scale}
273
- onChange={handleZoom}
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"