@os-design/upload 1.0.202 → 1.0.204
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-design/upload",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.204",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": "git@gitlab.com:os-team/libs/os-design.git",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"!**/*.test.ts",
|
|
20
|
+
"!**/*.test.tsx",
|
|
21
|
+
"!**/__tests__",
|
|
22
|
+
"!**/*.stories.tsx",
|
|
23
|
+
"!**/*.stories.mdx",
|
|
24
|
+
"!**/*.example.tsx"
|
|
18
25
|
],
|
|
19
26
|
"sideEffects": false,
|
|
20
27
|
"scripts": {
|
|
@@ -29,11 +36,11 @@
|
|
|
29
36
|
"access": "public"
|
|
30
37
|
},
|
|
31
38
|
"dependencies": {
|
|
32
|
-
"@os-design/core": "^1.0.
|
|
33
|
-
"@os-design/icons": "^1.0.
|
|
34
|
-
"@os-design/styles": "^1.0.
|
|
35
|
-
"@os-design/theming": "^1.0.
|
|
36
|
-
"@os-design/utils": "^1.0.
|
|
39
|
+
"@os-design/core": "^1.0.201",
|
|
40
|
+
"@os-design/icons": "^1.0.49",
|
|
41
|
+
"@os-design/styles": "^1.0.46",
|
|
42
|
+
"@os-design/theming": "^1.0.44",
|
|
43
|
+
"@os-design/utils": "^1.0.63",
|
|
37
44
|
"react-dropzone": "^14.2.3"
|
|
38
45
|
},
|
|
39
46
|
"devDependencies": {
|
|
@@ -46,5 +53,5 @@
|
|
|
46
53
|
"react": ">=18",
|
|
47
54
|
"react-dom": ">=18"
|
|
48
55
|
},
|
|
49
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "e5d8409760608145d2c738aa5789d0465ae5416f"
|
|
50
57
|
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { css } from '@emotion/react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import { Button, Image } from '@os-design/core';
|
|
4
|
+
import { Delete, Picture } from '@os-design/icons';
|
|
5
|
+
import {
|
|
6
|
+
WithSize,
|
|
7
|
+
resetFocusStyles,
|
|
8
|
+
sizeStyles,
|
|
9
|
+
transitionStyles,
|
|
10
|
+
} from '@os-design/styles';
|
|
11
|
+
import { ThemeOverrider, clr } from '@os-design/theming';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
isTouchDevice,
|
|
15
|
+
omitEmotionProps,
|
|
16
|
+
useForwardedState,
|
|
17
|
+
} from '@os-design/utils';
|
|
18
|
+
import React, { forwardRef, useCallback, useMemo } from 'react';
|
|
19
|
+
import { Accept, useDropzone } from 'react-dropzone';
|
|
20
|
+
import defaultLocale, { ImageUploadLocale } from './utils/defaultLocale';
|
|
21
|
+
|
|
22
|
+
type JsxDivProps = Omit<
|
|
23
|
+
JSX.IntrinsicElements['div'],
|
|
24
|
+
'defaultValue' | 'onChange' | 'ref'
|
|
25
|
+
>;
|
|
26
|
+
export interface ImageUploadProps extends JsxDivProps, WithSize {
|
|
27
|
+
/**
|
|
28
|
+
* The url of the image.
|
|
29
|
+
* @default undefined
|
|
30
|
+
*/
|
|
31
|
+
url?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Allowed image formats.
|
|
34
|
+
* @default undefined
|
|
35
|
+
*/
|
|
36
|
+
accept?: Accept;
|
|
37
|
+
/**
|
|
38
|
+
* The locale of the component.
|
|
39
|
+
* @default undefined
|
|
40
|
+
*/
|
|
41
|
+
locale?: ImageUploadLocale;
|
|
42
|
+
/**
|
|
43
|
+
* The selected local file, or null if the image is marked as deleted.
|
|
44
|
+
* @default undefined
|
|
45
|
+
*/
|
|
46
|
+
value?: File | null;
|
|
47
|
+
/**
|
|
48
|
+
* The default value.
|
|
49
|
+
* @default undefined
|
|
50
|
+
*/
|
|
51
|
+
defaultValue?: File | null;
|
|
52
|
+
/**
|
|
53
|
+
* The change event handler.
|
|
54
|
+
* @default undefined
|
|
55
|
+
*/
|
|
56
|
+
onChange?: (value: File | null) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const overlayHasImageStyles = (p) =>
|
|
60
|
+
p.hasImage &&
|
|
61
|
+
css`
|
|
62
|
+
background-color: hsla(0, 0%, 0%, ${p.theme.imageUploadOverlayOpacity});
|
|
63
|
+
color: hsl(0, 0%, 100%);
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const overlayHasImageHoverStyles = (p) =>
|
|
67
|
+
p.hasImage &&
|
|
68
|
+
!p.isDragActive &&
|
|
69
|
+
css`
|
|
70
|
+
@media (hover: hover) {
|
|
71
|
+
opacity: 0;
|
|
72
|
+
|
|
73
|
+
&:hover {
|
|
74
|
+
opacity: 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
interface OverlayProps {
|
|
80
|
+
isDragActive: boolean;
|
|
81
|
+
hasImage: boolean;
|
|
82
|
+
}
|
|
83
|
+
export const Overlay = styled(
|
|
84
|
+
'div',
|
|
85
|
+
omitEmotionProps('isDragActive', 'hasImage')
|
|
86
|
+
)<OverlayProps>`
|
|
87
|
+
${resetFocusStyles};
|
|
88
|
+
position: absolute;
|
|
89
|
+
top: 0;
|
|
90
|
+
right: 0;
|
|
91
|
+
bottom: 0;
|
|
92
|
+
left: 0;
|
|
93
|
+
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
align-items: center;
|
|
98
|
+
|
|
99
|
+
text-align: center;
|
|
100
|
+
padding: ${(p) => p.theme.imageUploadOverlayPadding}em;
|
|
101
|
+
|
|
102
|
+
${overlayHasImageStyles};
|
|
103
|
+
${overlayHasImageHoverStyles}
|
|
104
|
+
${transitionStyles('opacity')};
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const contentIsDragActiveStyles = (p) =>
|
|
108
|
+
p.isDragActive &&
|
|
109
|
+
css`
|
|
110
|
+
transform: scale(0.92);
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
type ContentProps = Pick<OverlayProps, 'isDragActive'>;
|
|
114
|
+
const Content = styled('div', omitEmotionProps('isDragActive'))<ContentProps>`
|
|
115
|
+
${contentIsDragActiveStyles};
|
|
116
|
+
${transitionStyles('transform')};
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
const PictureIcon = styled(Picture)`
|
|
120
|
+
opacity: 0.8;
|
|
121
|
+
font-size: 2.5em;
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const Title = styled.div`
|
|
125
|
+
opacity: 0.8;
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const DeleteButtonContainer = styled.div`
|
|
129
|
+
position: absolute;
|
|
130
|
+
top: 0.5em;
|
|
131
|
+
right: 0.5em;
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
const hoverStyles = (p) => css`
|
|
135
|
+
border-color: ${clr(p.theme.imageUploadNoImageHoverColorBorder)};
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const notHasImageStyles = (p) =>
|
|
139
|
+
!p.hasImage &&
|
|
140
|
+
css`
|
|
141
|
+
background-color: ${clr(p.theme.imageUploadNoImageColorBg)};
|
|
142
|
+
border: 2px dashed ${clr(p.theme.imageUploadNoImageColorBorder)};
|
|
143
|
+
|
|
144
|
+
@media (hover: hover) {
|
|
145
|
+
&:hover {
|
|
146
|
+
${hoverStyles(p)};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const focusStyles = (p) => css`
|
|
152
|
+
&:focus-within {
|
|
153
|
+
& > div {
|
|
154
|
+
opacity: 1;
|
|
155
|
+
}
|
|
156
|
+
${hoverStyles(p)};
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
const isDragActiveStyles = (p) =>
|
|
161
|
+
p.isDragActive &&
|
|
162
|
+
css`
|
|
163
|
+
${hoverStyles(p)};
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
const LocalImage = styled.img`
|
|
167
|
+
width: 100%;
|
|
168
|
+
vertical-align: top;
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
const RemoteImage = styled(Image)`
|
|
172
|
+
vertical-align: top;
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
interface ContainerProps extends WithSize {
|
|
176
|
+
isDragActive: boolean;
|
|
177
|
+
hasImage: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const Container = styled(
|
|
181
|
+
'div',
|
|
182
|
+
omitEmotionProps('isDragActive', 'hasImage', 'size')
|
|
183
|
+
)<ContainerProps>`
|
|
184
|
+
position: relative;
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
box-sizing: border-box;
|
|
188
|
+
|
|
189
|
+
max-width: ${(p) => p.theme.imageUploadMaxWidth}em;
|
|
190
|
+
min-height: ${(p) => p.theme.imageUploadMinHeight}em;
|
|
191
|
+
border-radius: ${(p) => p.theme.borderRadius}em;
|
|
192
|
+
color: ${(p) => clr(p.theme.colorText)};
|
|
193
|
+
|
|
194
|
+
${notHasImageStyles};
|
|
195
|
+
${focusStyles};
|
|
196
|
+
${isDragActiveStyles};
|
|
197
|
+
${sizeStyles};
|
|
198
|
+
${transitionStyles('border')};
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The component to upload an image.
|
|
203
|
+
*/
|
|
204
|
+
const ImageUpload = forwardRef<HTMLDivElement, ImageUploadProps>(
|
|
205
|
+
(
|
|
206
|
+
{
|
|
207
|
+
url,
|
|
208
|
+
accept = { 'image/*': ['.jpeg', '.png', '.webp'] },
|
|
209
|
+
locale = defaultLocale,
|
|
210
|
+
value,
|
|
211
|
+
defaultValue,
|
|
212
|
+
onChange,
|
|
213
|
+
size,
|
|
214
|
+
...rest
|
|
215
|
+
},
|
|
216
|
+
ref
|
|
217
|
+
) => {
|
|
218
|
+
const touchDevice = useMemo(() => isTouchDevice(), []);
|
|
219
|
+
const [forwardedValue, setForwardedValue] = useForwardedState({
|
|
220
|
+
value,
|
|
221
|
+
defaultValue,
|
|
222
|
+
onChange,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
226
|
+
accept,
|
|
227
|
+
multiple: false,
|
|
228
|
+
onDrop: ([file]) => setForwardedValue(file),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const source = useMemo(() => {
|
|
232
|
+
if (forwardedValue === null) return null; // If the user select an image
|
|
233
|
+
if (forwardedValue) return URL.createObjectURL(forwardedValue); // If the image already exists
|
|
234
|
+
if (url) return url;
|
|
235
|
+
return null;
|
|
236
|
+
}, [forwardedValue, url]);
|
|
237
|
+
|
|
238
|
+
const hasImage = useMemo(() => !!source, [source]);
|
|
239
|
+
|
|
240
|
+
const renderImage = useCallback(() => {
|
|
241
|
+
if (!source) return null;
|
|
242
|
+
if (typeof forwardedValue === 'object' && forwardedValue !== null) {
|
|
243
|
+
return <LocalImage src={source} />;
|
|
244
|
+
}
|
|
245
|
+
return <RemoteImage url={source} />;
|
|
246
|
+
}, [forwardedValue, source]);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<Container
|
|
250
|
+
isDragActive={isDragActive}
|
|
251
|
+
hasImage={hasImage}
|
|
252
|
+
size={size}
|
|
253
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
254
|
+
{...rest}
|
|
255
|
+
ref={ref}
|
|
256
|
+
>
|
|
257
|
+
<input {...getInputProps()} />
|
|
258
|
+
{renderImage()}
|
|
259
|
+
|
|
260
|
+
<Overlay
|
|
261
|
+
isDragActive={isDragActive}
|
|
262
|
+
hasImage={hasImage}
|
|
263
|
+
{
|
|
264
|
+
...getRootProps({
|
|
265
|
+
onKeyDown: (e) => {
|
|
266
|
+
if (e.key === 'Backspace') setForwardedValue(null);
|
|
267
|
+
},
|
|
268
|
+
} as any) // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
269
|
+
}
|
|
270
|
+
>
|
|
271
|
+
<Content isDragActive={isDragActive}>
|
|
272
|
+
<PictureIcon />
|
|
273
|
+
<Title>{touchDevice ? locale.touchTitle : locale.dropTitle}</Title>
|
|
274
|
+
</Content>
|
|
275
|
+
|
|
276
|
+
{hasImage && (
|
|
277
|
+
<ThemeOverrider
|
|
278
|
+
overrides={{
|
|
279
|
+
buttonGhostColorText: [0, 0, 100],
|
|
280
|
+
buttonGhostColorBgHover: [0, 0, 100, 0.1],
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
<DeleteButtonContainer>
|
|
284
|
+
<Button
|
|
285
|
+
type='ghost'
|
|
286
|
+
wide='never'
|
|
287
|
+
onClick={(e) => {
|
|
288
|
+
setForwardedValue(null);
|
|
289
|
+
e.stopPropagation();
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
<Delete />
|
|
293
|
+
</Button>
|
|
294
|
+
</DeleteButtonContainer>
|
|
295
|
+
</ThemeOverrider>
|
|
296
|
+
)}
|
|
297
|
+
</Overlay>
|
|
298
|
+
</Container>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
ImageUpload.displayName = 'ImageUpload';
|
|
304
|
+
|
|
305
|
+
export default ImageUpload;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ImageUploadLocale {
|
|
2
|
+
touchTitle: string;
|
|
3
|
+
dropTitle: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const defaultLocale: ImageUploadLocale = {
|
|
7
|
+
touchTitle: 'Click here to choose an image',
|
|
8
|
+
dropTitle: 'Drop an image here or click to choose it',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default defaultLocale;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import styled from '@emotion/styled';
|
|
2
|
+
import { Skeleton, SkeletonProps } from '@os-design/core';
|
|
3
|
+
|
|
4
|
+
import { sizeStyles, WithSize } from '@os-design/styles';
|
|
5
|
+
import { omitEmotionProps } from '@os-design/utils';
|
|
6
|
+
import React, { forwardRef } from 'react';
|
|
7
|
+
|
|
8
|
+
export type ImageUploadSkeletonProps = Omit<SkeletonProps, 'width'> & WithSize;
|
|
9
|
+
|
|
10
|
+
const StyledImageUploadSkeleton = styled(
|
|
11
|
+
Skeleton,
|
|
12
|
+
omitEmotionProps('size')
|
|
13
|
+
)<WithSize>`
|
|
14
|
+
max-width: ${(p) => p.theme.imageUploadMaxWidth}em;
|
|
15
|
+
height: ${(p) => p.theme.imageUploadMinHeight}em;
|
|
16
|
+
${sizeStyles};
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provides an image upload placeholder while a user waits for
|
|
21
|
+
* the content to load.
|
|
22
|
+
*/
|
|
23
|
+
const ImageUploadSkeleton = forwardRef<
|
|
24
|
+
HTMLDivElement,
|
|
25
|
+
ImageUploadSkeletonProps
|
|
26
|
+
>((props, ref) => (
|
|
27
|
+
<StyledImageUploadSkeleton width='100%' {...props} ref={ref} />
|
|
28
|
+
));
|
|
29
|
+
|
|
30
|
+
ImageUploadSkeleton.displayName = 'ImageUploadSkeleton';
|
|
31
|
+
|
|
32
|
+
export default ImageUploadSkeleton;
|
package/src/index.ts
ADDED