@os-design-mobile/image-upload 1.0.67 → 1.0.69
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/package.json +19 -11
- package/src/@types/emotion.d.ts +7 -0
- package/src/index.tsx +294 -0
- package/src/utils/defaultLocale.ts +13 -0
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-design-mobile/image-upload",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.69",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": "git@gitlab.com:os-team/libs/os-design-mobile.git",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"react-native": "src/index.tsx",
|
|
8
9
|
"files": [
|
|
9
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"!**/*.test.ts",
|
|
13
|
+
"!**/*.test.tsx",
|
|
14
|
+
"!**/__tests__",
|
|
15
|
+
"!**/*.stories.tsx",
|
|
16
|
+
"!**/*.stories.mdx",
|
|
17
|
+
"!**/*.example.tsx"
|
|
10
18
|
],
|
|
11
19
|
"scripts": {
|
|
12
20
|
"clean": "rimraf dist",
|
|
@@ -19,14 +27,14 @@
|
|
|
19
27
|
"access": "public"
|
|
20
28
|
},
|
|
21
29
|
"dependencies": {
|
|
22
|
-
"@os-design-mobile/action-menu": "^1.0.
|
|
23
|
-
"@os-design-mobile/button": "^1.0.
|
|
24
|
-
"@os-design-mobile/icons": "^1.0.
|
|
25
|
-
"@os-design-mobile/image": "^1.0.
|
|
26
|
-
"@os-design-mobile/text": "^1.0.
|
|
27
|
-
"@os-design-mobile/theming": "^1.0.
|
|
28
|
-
"@os-design/omit-emotion-props": "^1.0.
|
|
29
|
-
"@os-design/use-forwarded-state": "^1.0.
|
|
30
|
+
"@os-design-mobile/action-menu": "^1.0.68",
|
|
31
|
+
"@os-design-mobile/button": "^1.0.51",
|
|
32
|
+
"@os-design-mobile/icons": "^1.0.49",
|
|
33
|
+
"@os-design-mobile/image": "^1.0.35",
|
|
34
|
+
"@os-design-mobile/text": "^1.0.47",
|
|
35
|
+
"@os-design-mobile/theming": "^1.0.35",
|
|
36
|
+
"@os-design/omit-emotion-props": "^1.0.13",
|
|
37
|
+
"@os-design/use-forwarded-state": "^1.0.15"
|
|
30
38
|
},
|
|
31
39
|
"peerDependencies": {
|
|
32
40
|
"@emotion/native": ">=11",
|
|
@@ -39,5 +47,5 @@
|
|
|
39
47
|
"react-native-safe-area-context": ">=3",
|
|
40
48
|
"react-native-svg": ">=12"
|
|
41
49
|
},
|
|
42
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "ea2e9015ca3ac20795b3fc1303df9016e0c868db"
|
|
43
51
|
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { View, ViewProps } from 'react-native';
|
|
3
|
+
import { RectButton, RectButtonProps } from 'react-native-gesture-handler';
|
|
4
|
+
import ActionMenu, { MenuItem } from '@os-design-mobile/action-menu';
|
|
5
|
+
import { Picture, Camera, Delete } from '@os-design-mobile/icons';
|
|
6
|
+
import {
|
|
7
|
+
launchImageLibrary,
|
|
8
|
+
launchCamera,
|
|
9
|
+
Asset,
|
|
10
|
+
ImagePickerResponse,
|
|
11
|
+
} from 'react-native-image-picker';
|
|
12
|
+
import Text from '@os-design-mobile/text';
|
|
13
|
+
import Image from '@os-design-mobile/image';
|
|
14
|
+
import useForwardedState from '@os-design/use-forwarded-state';
|
|
15
|
+
import styled from '@emotion/native';
|
|
16
|
+
import omitEmotionProps from '@os-design/omit-emotion-props';
|
|
17
|
+
import { clr, ThemeOverrider, useTheme } from '@os-design-mobile/theming';
|
|
18
|
+
import Button from '@os-design-mobile/button';
|
|
19
|
+
import {
|
|
20
|
+
CameraOptions,
|
|
21
|
+
ImageLibraryOptions,
|
|
22
|
+
} from 'react-native-image-picker/src/types';
|
|
23
|
+
import defaultLocale, { ImageUploadLocale } from './utils/defaultLocale';
|
|
24
|
+
|
|
25
|
+
export interface ImageUploadProps extends RectButtonProps {
|
|
26
|
+
/**
|
|
27
|
+
* The url of the image.
|
|
28
|
+
* @default undefined
|
|
29
|
+
*/
|
|
30
|
+
url?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The locale of the component.
|
|
33
|
+
* @default undefined
|
|
34
|
+
*/
|
|
35
|
+
locale?: ImageUploadLocale;
|
|
36
|
+
/**
|
|
37
|
+
* The image library options.
|
|
38
|
+
* @default undefined
|
|
39
|
+
*/
|
|
40
|
+
imageLibraryOptions?: Pick<
|
|
41
|
+
ImageLibraryOptions,
|
|
42
|
+
'maxWidth' | 'maxHeight' | 'quality'
|
|
43
|
+
>;
|
|
44
|
+
/**
|
|
45
|
+
* The camera options.
|
|
46
|
+
* @default undefined
|
|
47
|
+
*/
|
|
48
|
+
cameraOptions?: Pick<
|
|
49
|
+
CameraOptions,
|
|
50
|
+
'maxWidth' | 'maxHeight' | 'quality' | 'saveToPhotos' | 'cameraType'
|
|
51
|
+
>;
|
|
52
|
+
/**
|
|
53
|
+
* The selected local file, or null if the image is marked as deleted.
|
|
54
|
+
* @default undefined
|
|
55
|
+
*/
|
|
56
|
+
value?: Asset | null;
|
|
57
|
+
/**
|
|
58
|
+
* The default value.
|
|
59
|
+
* @default undefined
|
|
60
|
+
*/
|
|
61
|
+
defaultValue?: Asset | null;
|
|
62
|
+
/**
|
|
63
|
+
* The change event handler.
|
|
64
|
+
* @default undefined
|
|
65
|
+
*/
|
|
66
|
+
onChange?: (value: Asset | null) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const overlayHasImageStyles = (p) =>
|
|
70
|
+
p.hasImage
|
|
71
|
+
? {
|
|
72
|
+
backgroundColor: clr([0, 0, 0, p.theme.imageUploadOverlayOpacity]),
|
|
73
|
+
borderRadius: p.theme.borderRadius * p.theme.fontSize,
|
|
74
|
+
}
|
|
75
|
+
: {};
|
|
76
|
+
|
|
77
|
+
interface OverlayProps {
|
|
78
|
+
hasImage: boolean;
|
|
79
|
+
}
|
|
80
|
+
const Overlay = styled(
|
|
81
|
+
View,
|
|
82
|
+
omitEmotionProps('hasImage')
|
|
83
|
+
)<OverlayProps>((p) => ({
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
top: 0,
|
|
86
|
+
right: 0,
|
|
87
|
+
bottom: 0,
|
|
88
|
+
left: 0,
|
|
89
|
+
justifyContent: 'center',
|
|
90
|
+
alignItems: 'center',
|
|
91
|
+
padding: p.theme.imageUploadOverlayPadding * p.theme.fontSize,
|
|
92
|
+
...overlayHasImageStyles(p),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
const StyledRectButton = styled(RectButton)((p) => ({
|
|
96
|
+
width: '100%',
|
|
97
|
+
maxWidth: p.theme.imageUploadMaxWidth * p.theme.fontSize,
|
|
98
|
+
minHeight: p.theme.imageUploadMinHeight * p.theme.fontSize,
|
|
99
|
+
borderRadius: p.theme.borderRadius * p.theme.fontSize,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
const notHasImageStyles = (p): Partial<ViewProps> =>
|
|
103
|
+
!p.hasImage
|
|
104
|
+
? ({
|
|
105
|
+
backgroundColor: clr(p.theme.imageUploadNoImageColorBg),
|
|
106
|
+
borderWidth: 2,
|
|
107
|
+
borderColor: clr(p.theme.imageUploadNoImageColorBorder),
|
|
108
|
+
borderStyle: 'dashed',
|
|
109
|
+
} as Partial<ViewProps>)
|
|
110
|
+
: {};
|
|
111
|
+
|
|
112
|
+
interface ContainerProps {
|
|
113
|
+
hasImage: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const Container = styled(
|
|
117
|
+
View,
|
|
118
|
+
omitEmotionProps('hasImage')
|
|
119
|
+
)<ContainerProps>((p) => ({
|
|
120
|
+
minHeight: p.theme.imageUploadMinHeight * p.theme.fontSize,
|
|
121
|
+
borderRadius: p.theme.borderRadius * p.theme.fontSize,
|
|
122
|
+
...notHasImageStyles(p),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const Content = styled.View({
|
|
126
|
+
flex: 1,
|
|
127
|
+
justifyContent: 'center',
|
|
128
|
+
alignItems: 'center',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const Title = styled(Text)((p) => ({
|
|
132
|
+
marginTop: 0.5 * p.theme.fontSize,
|
|
133
|
+
textAlign: 'center',
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
const StyledImage = styled(Image)({
|
|
137
|
+
resizeMode: 'cover',
|
|
138
|
+
aspectRatio: 1,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const DeleteButtonContainer = styled.View((p) => ({
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
top: 0.5 * p.theme.fontSize,
|
|
144
|
+
right: 0.5 * p.theme.fontSize,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The component to upload an image.
|
|
149
|
+
*/
|
|
150
|
+
const ImageUpload = forwardRef<RectButton, ImageUploadProps>(
|
|
151
|
+
(
|
|
152
|
+
{
|
|
153
|
+
url,
|
|
154
|
+
locale = defaultLocale,
|
|
155
|
+
imageLibraryOptions = {},
|
|
156
|
+
cameraOptions = {},
|
|
157
|
+
value,
|
|
158
|
+
defaultValue,
|
|
159
|
+
onChange,
|
|
160
|
+
onPress = () => {},
|
|
161
|
+
...rest
|
|
162
|
+
},
|
|
163
|
+
ref
|
|
164
|
+
) => {
|
|
165
|
+
const [forwardedValue, setForwardedValue] = useForwardedState({
|
|
166
|
+
value,
|
|
167
|
+
defaultValue,
|
|
168
|
+
onChange,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
172
|
+
const { theme } = useTheme();
|
|
173
|
+
|
|
174
|
+
const chooseFromPhotoLibraryHandler = useCallback(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const { assets } = await new Promise<ImagePickerResponse>((resolve) => {
|
|
177
|
+
launchImageLibrary(
|
|
178
|
+
{
|
|
179
|
+
mediaType: 'photo',
|
|
180
|
+
...imageLibraryOptions,
|
|
181
|
+
},
|
|
182
|
+
resolve
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
setModalVisible(false);
|
|
186
|
+
if (assets) setForwardedValue(assets[0]);
|
|
187
|
+
} catch (e) {} // eslint-disable-line no-empty
|
|
188
|
+
}, [imageLibraryOptions, setForwardedValue]);
|
|
189
|
+
|
|
190
|
+
const takePhotoHandler = useCallback(async () => {
|
|
191
|
+
try {
|
|
192
|
+
const { assets } = await new Promise<ImagePickerResponse>((resolve) => {
|
|
193
|
+
launchCamera(
|
|
194
|
+
{
|
|
195
|
+
mediaType: 'photo',
|
|
196
|
+
...cameraOptions,
|
|
197
|
+
},
|
|
198
|
+
resolve
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
setModalVisible(false);
|
|
202
|
+
if (assets) setForwardedValue(assets[0]);
|
|
203
|
+
} catch (e) {} // eslint-disable-line no-empty
|
|
204
|
+
}, [cameraOptions, setForwardedValue]);
|
|
205
|
+
|
|
206
|
+
const source = useMemo(() => {
|
|
207
|
+
if (forwardedValue === null) return null;
|
|
208
|
+
if (forwardedValue) return forwardedValue.uri; // If the user select an image
|
|
209
|
+
if (url) return url; // If the image already exists
|
|
210
|
+
return null;
|
|
211
|
+
}, [forwardedValue, url]);
|
|
212
|
+
|
|
213
|
+
const hasImage = useMemo(() => !!source, [source]);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<>
|
|
217
|
+
<ThemeOverrider overrides={hasImage ? { colorText: [0, 0, 100] } : {}}>
|
|
218
|
+
<StyledRectButton
|
|
219
|
+
onPress={(e) => {
|
|
220
|
+
setModalVisible(true);
|
|
221
|
+
onPress(e);
|
|
222
|
+
}}
|
|
223
|
+
underlayColor={
|
|
224
|
+
hasImage
|
|
225
|
+
? clr([0, 0, 100])
|
|
226
|
+
: clr(theme.imageUploadNoImageBgHighlight)
|
|
227
|
+
}
|
|
228
|
+
rippleColor={
|
|
229
|
+
hasImage
|
|
230
|
+
? clr([0, 0, 100])
|
|
231
|
+
: clr(theme.imageUploadNoImageBgRipple)
|
|
232
|
+
}
|
|
233
|
+
activeOpacity={hasImage ? 0.8 : 1}
|
|
234
|
+
{...rest}
|
|
235
|
+
ref={ref}
|
|
236
|
+
>
|
|
237
|
+
<Container
|
|
238
|
+
hasImage={hasImage}
|
|
239
|
+
accessible
|
|
240
|
+
accessibilityRole='imagebutton'
|
|
241
|
+
>
|
|
242
|
+
{source && <StyledImage source={{ uri: source }} />}
|
|
243
|
+
|
|
244
|
+
<Overlay hasImage={hasImage}>
|
|
245
|
+
<Content>
|
|
246
|
+
<ThemeOverrider
|
|
247
|
+
overrides={(t) => ({ fontSize: 2.5 * t.fontSize })}
|
|
248
|
+
>
|
|
249
|
+
<Picture />
|
|
250
|
+
</ThemeOverrider>
|
|
251
|
+
<Title>{locale?.title}</Title>
|
|
252
|
+
</Content>
|
|
253
|
+
|
|
254
|
+
{hasImage && (
|
|
255
|
+
<ThemeOverrider
|
|
256
|
+
overrides={{
|
|
257
|
+
buttonGhostColorText: [0, 0, 100],
|
|
258
|
+
buttonGhostColorBgHighlight: [0, 0, 100, 0.1],
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
<DeleteButtonContainer>
|
|
262
|
+
<Button
|
|
263
|
+
type='ghost'
|
|
264
|
+
onPress={() => setForwardedValue(null)}
|
|
265
|
+
>
|
|
266
|
+
<Delete />
|
|
267
|
+
</Button>
|
|
268
|
+
</DeleteButtonContainer>
|
|
269
|
+
</ThemeOverrider>
|
|
270
|
+
)}
|
|
271
|
+
</Overlay>
|
|
272
|
+
</Container>
|
|
273
|
+
</StyledRectButton>
|
|
274
|
+
</ThemeOverrider>
|
|
275
|
+
|
|
276
|
+
<ActionMenu
|
|
277
|
+
visible={modalVisible}
|
|
278
|
+
onClose={() => setModalVisible(false)}
|
|
279
|
+
>
|
|
280
|
+
<MenuItem left={<Picture />} onPress={chooseFromPhotoLibraryHandler}>
|
|
281
|
+
{locale?.chooseFromLibrary}
|
|
282
|
+
</MenuItem>
|
|
283
|
+
<MenuItem left={<Camera />} onPress={takePhotoHandler}>
|
|
284
|
+
{locale?.takePhoto}
|
|
285
|
+
</MenuItem>
|
|
286
|
+
</ActionMenu>
|
|
287
|
+
</>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
ImageUpload.displayName = 'ImageUpload';
|
|
293
|
+
|
|
294
|
+
export default ImageUpload;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ImageUploadLocale {
|
|
2
|
+
title: string;
|
|
3
|
+
chooseFromLibrary: string;
|
|
4
|
+
takePhoto: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const defaultLocale: ImageUploadLocale = {
|
|
8
|
+
title: 'Click here to choose an image',
|
|
9
|
+
chooseFromLibrary: 'Choose from Library',
|
|
10
|
+
takePhoto: 'Take a Photo',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default defaultLocale;
|