react-dropzone 11.2.3 → 11.3.2
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/README.md +11 -2
- package/dist/es/index.js +27 -10
- package/dist/index.js +2 -2
- package/examples/doka/README.md +140 -0
- package/examples/validator/README.md +68 -0
- package/package.json +5 -2
- package/src/index.js +25 -8
- package/src/index.spec.js +41 -0
- package/styleguide.config.js +23 -3
- package/typings/react-dropzone.d.ts +2 -1
- package/vendor/doka/doka.esm.min.js +10 -0
- package/vendor/doka/doka.min.css +11 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
If you'd like to integrate the dropzone with the [doka](https://pqina.nl/doka/?ref=react-dropzone) image editor, you just need to pass either of the selected images to the `create()` method exported by doka:
|
|
2
|
+
|
|
3
|
+
```jsx harmony
|
|
4
|
+
import React, {useEffect, useState} from 'react';
|
|
5
|
+
import {useDropzone} from 'react-dropzone';
|
|
6
|
+
|
|
7
|
+
import {create} from 'doka';
|
|
8
|
+
|
|
9
|
+
const thumbsContainer = {
|
|
10
|
+
display: "flex",
|
|
11
|
+
flexDirection: "row",
|
|
12
|
+
flexWrap: "wrap",
|
|
13
|
+
marginTop: 16,
|
|
14
|
+
padding: 20
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const thumb = {
|
|
18
|
+
position: "relative",
|
|
19
|
+
display: "inline-flex",
|
|
20
|
+
borderRadius: 2,
|
|
21
|
+
border: "1px solid #eaeaea",
|
|
22
|
+
marginBottom: 8,
|
|
23
|
+
marginRight: 8,
|
|
24
|
+
width: 100,
|
|
25
|
+
height: 100,
|
|
26
|
+
padding: 4,
|
|
27
|
+
boxSizing: "border-box"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const thumbInner = {
|
|
31
|
+
display: "flex",
|
|
32
|
+
minWidth: 0,
|
|
33
|
+
overflow: "hidden"
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const img = {
|
|
37
|
+
display: "block",
|
|
38
|
+
width: "auto",
|
|
39
|
+
height: "100%"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const thumbButton = {
|
|
43
|
+
position: "absolute",
|
|
44
|
+
right: 10,
|
|
45
|
+
bottom: 10,
|
|
46
|
+
background: "rgba(0,0,0,.8)",
|
|
47
|
+
color: "#fff",
|
|
48
|
+
border: 0,
|
|
49
|
+
borderRadius: ".325em",
|
|
50
|
+
cursor: "pointer"
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const editImage = (image, done) => {
|
|
54
|
+
const imageFile = image.doka ? image.doka.file : image;
|
|
55
|
+
const imageState = image.doka ? image.doka.data : {};
|
|
56
|
+
create({
|
|
57
|
+
// recreate previous state
|
|
58
|
+
...imageState,
|
|
59
|
+
|
|
60
|
+
// load original image file
|
|
61
|
+
src: imageFile,
|
|
62
|
+
outputData: true,
|
|
63
|
+
|
|
64
|
+
onconfirm: ({ file, data }) => {
|
|
65
|
+
Object.assign(file, {
|
|
66
|
+
doka: { file: imageFile, data }
|
|
67
|
+
});
|
|
68
|
+
done(file);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function Doka(props) {
|
|
74
|
+
const [files, setFiles] = useState([]);
|
|
75
|
+
const { getRootProps, getInputProps } = useDropzone({
|
|
76
|
+
accept: "image/*",
|
|
77
|
+
onDrop: (acceptedFiles) => {
|
|
78
|
+
setFiles(
|
|
79
|
+
acceptedFiles.map((file) =>
|
|
80
|
+
Object.assign(file, {
|
|
81
|
+
preview: URL.createObjectURL(file)
|
|
82
|
+
})
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const thumbs = files.map((file, index) => (
|
|
89
|
+
<div style={thumb} key={file.name}>
|
|
90
|
+
<div style={thumbInner}>
|
|
91
|
+
<img src={file.preview} style={img} alt="" />
|
|
92
|
+
</div>
|
|
93
|
+
<button
|
|
94
|
+
style={thumbButton}
|
|
95
|
+
onClick={() =>
|
|
96
|
+
editImage(file, (output) => {
|
|
97
|
+
const updatedFiles = [...files];
|
|
98
|
+
|
|
99
|
+
// replace original image with new image
|
|
100
|
+
updatedFiles[index] = output;
|
|
101
|
+
|
|
102
|
+
// revoke preview URL for old image
|
|
103
|
+
if (file.preview) URL.revokeObjectURL(file.preview);
|
|
104
|
+
|
|
105
|
+
// set new preview URL
|
|
106
|
+
Object.assign(output, {
|
|
107
|
+
preview: URL.createObjectURL(output)
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// update view
|
|
111
|
+
setFiles(updatedFiles);
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
edit
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
));
|
|
119
|
+
|
|
120
|
+
useEffect(
|
|
121
|
+
() => () => {
|
|
122
|
+
// Make sure to revoke the data uris to avoid memory leaks
|
|
123
|
+
files.forEach((file) => URL.revokeObjectURL(file.preview));
|
|
124
|
+
},
|
|
125
|
+
[files]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<section className="container">
|
|
130
|
+
<div {...getRootProps({ className: "dropzone" })}>
|
|
131
|
+
<input {...getInputProps()} />
|
|
132
|
+
<p>Drag 'n' drop some files here, or click to select files</p>
|
|
133
|
+
</div>
|
|
134
|
+
<aside style={thumbsContainer}>{thumbs}</aside>
|
|
135
|
+
</section>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
<Doka />
|
|
140
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
By providing `validator` prop you can specify custom validation for files.
|
|
2
|
+
|
|
3
|
+
The value must be a function that accepts File object and returns null if file should be accepted or error object/array of error objects if file should me rejected.
|
|
4
|
+
|
|
5
|
+
```jsx harmony
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import {useDropzone} from 'react-dropzone';
|
|
8
|
+
|
|
9
|
+
const maxLength = 20;
|
|
10
|
+
|
|
11
|
+
function nameLengthValidator(file) {
|
|
12
|
+
if (file.name.length > maxLength) {
|
|
13
|
+
return {
|
|
14
|
+
code: "name-too-large",
|
|
15
|
+
message: `Name is larger than ${maxLength} characters`
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function CustomValidation(props) {
|
|
23
|
+
const {
|
|
24
|
+
acceptedFiles,
|
|
25
|
+
fileRejections,
|
|
26
|
+
getRootProps,
|
|
27
|
+
getInputProps
|
|
28
|
+
} = useDropzone({
|
|
29
|
+
validator: nameLengthValidator
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const acceptedFileItems = acceptedFiles.map(file => (
|
|
33
|
+
<li key={file.path}>
|
|
34
|
+
{file.path} - {file.size} bytes
|
|
35
|
+
</li>
|
|
36
|
+
));
|
|
37
|
+
|
|
38
|
+
const fileRejectionItems = fileRejections.map(({ file, errors }) => (
|
|
39
|
+
<li key={file.path}>
|
|
40
|
+
{file.path} - {file.size} bytes
|
|
41
|
+
<ul>
|
|
42
|
+
{errors.map(e => (
|
|
43
|
+
<li key={e.code}>{e.message}</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
</li>
|
|
47
|
+
));
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<section className="container">
|
|
51
|
+
<div {...getRootProps({ className: 'dropzone' })}>
|
|
52
|
+
<input {...getInputProps()} />
|
|
53
|
+
<p>Drag 'n' drop some files here, or click to select files</p>
|
|
54
|
+
<em>(Only files with name less than 20 characters will be accepted)</em>
|
|
55
|
+
</div>
|
|
56
|
+
<aside>
|
|
57
|
+
<h4>Accepted files</h4>
|
|
58
|
+
<ul>{acceptedFileItems}</ul>
|
|
59
|
+
<h4>Rejected files</h4>
|
|
60
|
+
<ul>{fileRejectionItems}</ul>
|
|
61
|
+
</aside>
|
|
62
|
+
</section>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
<CustomValidation />
|
|
67
|
+
```
|
|
68
|
+
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -60,7 +60,8 @@ const defaultProps = {
|
|
|
60
60
|
noClick: false,
|
|
61
61
|
noKeyboard: false,
|
|
62
62
|
noDrag: false,
|
|
63
|
-
noDragEventsBubbling: false
|
|
63
|
+
noDragEventsBubbling: false,
|
|
64
|
+
validator: null
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
Dropzone.defaultProps = defaultProps
|
|
@@ -226,7 +227,14 @@ Dropzone.propTypes = {
|
|
|
226
227
|
* @param {FileRejection[]} fileRejections
|
|
227
228
|
* @param {(DragEvent|Event)} event
|
|
228
229
|
*/
|
|
229
|
-
onDropRejected: PropTypes.func
|
|
230
|
+
onDropRejected: PropTypes.func,
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Custom validation function
|
|
234
|
+
* @param {File} file
|
|
235
|
+
* @returns {FileError|FileError[]}
|
|
236
|
+
*/
|
|
237
|
+
validator: PropTypes.func
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
export default Dropzone
|
|
@@ -398,7 +406,8 @@ export function useDropzone(options = {}) {
|
|
|
398
406
|
noClick,
|
|
399
407
|
noKeyboard,
|
|
400
408
|
noDrag,
|
|
401
|
-
noDragEventsBubbling
|
|
409
|
+
noDragEventsBubbling,
|
|
410
|
+
validator
|
|
402
411
|
} = {
|
|
403
412
|
...defaultProps,
|
|
404
413
|
...options
|
|
@@ -545,13 +554,14 @@ export function useDropzone(options = {}) {
|
|
|
545
554
|
event.persist()
|
|
546
555
|
stopPropagation(event)
|
|
547
556
|
|
|
548
|
-
|
|
557
|
+
const hasFiles = isEvtWithFiles(event);
|
|
558
|
+
if (hasFiles && event.dataTransfer) {
|
|
549
559
|
try {
|
|
550
560
|
event.dataTransfer.dropEffect = 'copy'
|
|
551
561
|
} catch {} /* eslint-disable-line no-empty */
|
|
552
562
|
}
|
|
553
563
|
|
|
554
|
-
if (
|
|
564
|
+
if (hasFiles && onDragOver) {
|
|
555
565
|
onDragOver(event)
|
|
556
566
|
}
|
|
557
567
|
|
|
@@ -615,11 +625,18 @@ export function useDropzone(options = {}) {
|
|
|
615
625
|
files.forEach(file => {
|
|
616
626
|
const [accepted, acceptError] = fileAccepted(file, accept)
|
|
617
627
|
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize)
|
|
618
|
-
|
|
628
|
+
const customErrors = validator ? validator(file) : null;
|
|
629
|
+
|
|
630
|
+
if (accepted && sizeMatch && !customErrors) {
|
|
619
631
|
acceptedFiles.push(file)
|
|
620
632
|
} else {
|
|
621
|
-
|
|
622
|
-
|
|
633
|
+
let errors = [acceptError, sizeError];
|
|
634
|
+
|
|
635
|
+
if (customErrors) {
|
|
636
|
+
errors = errors.concat(customErrors);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
fileRejections.push({ file, errors: errors.filter(e => e) })
|
|
623
640
|
}
|
|
624
641
|
})
|
|
625
642
|
|
package/src/index.spec.js
CHANGED
|
@@ -2719,6 +2719,47 @@ describe('useDropzone() hook', () => {
|
|
|
2719
2719
|
expect(fn).not.toThrow()
|
|
2720
2720
|
})
|
|
2721
2721
|
})
|
|
2722
|
+
|
|
2723
|
+
describe('validator', () => {
|
|
2724
|
+
it('rejects with custom error', async () => {
|
|
2725
|
+
const validator = file => {
|
|
2726
|
+
if (/dogs/i.test(file.name))
|
|
2727
|
+
return { code: 'dogs-not-allowed', message: 'Dogs not allowed' };
|
|
2728
|
+
|
|
2729
|
+
return null;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
const onDropSpy = jest.fn()
|
|
2733
|
+
|
|
2734
|
+
const ui = (
|
|
2735
|
+
<Dropzone validator={validator} onDrop={onDropSpy} multiple={true}>
|
|
2736
|
+
{({ getRootProps, getInputProps }) => (
|
|
2737
|
+
<div {...getRootProps()}>
|
|
2738
|
+
<input {...getInputProps()} />
|
|
2739
|
+
</div>
|
|
2740
|
+
)}
|
|
2741
|
+
</Dropzone>
|
|
2742
|
+
)
|
|
2743
|
+
|
|
2744
|
+
const { container, rerender } = render(ui)
|
|
2745
|
+
const dropzone = container.querySelector('div')
|
|
2746
|
+
|
|
2747
|
+
fireDrop(dropzone, createDtWithFiles(images))
|
|
2748
|
+
await flushPromises(rerender, ui)
|
|
2749
|
+
|
|
2750
|
+
expect(onDropSpy).toHaveBeenCalledWith([images[0]], [
|
|
2751
|
+
{
|
|
2752
|
+
file: images[1],
|
|
2753
|
+
errors: [
|
|
2754
|
+
{
|
|
2755
|
+
code: 'dogs-not-allowed',
|
|
2756
|
+
message: 'Dogs not allowed',
|
|
2757
|
+
}
|
|
2758
|
+
]
|
|
2759
|
+
}
|
|
2760
|
+
], expect.anything())
|
|
2761
|
+
})
|
|
2762
|
+
})
|
|
2722
2763
|
})
|
|
2723
2764
|
|
|
2724
2765
|
async function flushPromises(rerender, ui) {
|
package/styleguide.config.js
CHANGED
|
@@ -6,18 +6,25 @@ const { createConfig, babel, css, devServer } = require('webpack-blocks')
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
title: 'react-dropzone',
|
|
8
8
|
styleguideDir: path.join(__dirname, 'styleguide'),
|
|
9
|
+
template: {
|
|
10
|
+
favicon: 'https://github.com/react-dropzone/react-dropzone/raw/master/logo/logo.png'
|
|
11
|
+
},
|
|
9
12
|
webpackConfig: createConfig([babel(), css(), devServer({
|
|
10
13
|
disableHostCheck: true,
|
|
11
14
|
host: '0.0.0.0',
|
|
12
15
|
})]),
|
|
13
16
|
exampleMode: 'expand',
|
|
14
17
|
usageMode: 'expand',
|
|
15
|
-
showSidebar:
|
|
18
|
+
showSidebar: true,
|
|
16
19
|
serverPort: 8080,
|
|
17
20
|
moduleAliases: {
|
|
18
|
-
'react-dropzone': path.resolve(__dirname, './src')
|
|
21
|
+
'react-dropzone': path.resolve(__dirname, './src'),
|
|
22
|
+
'doka': path.resolve(__dirname, './vendor/doka/doka.esm.min.js')
|
|
19
23
|
},
|
|
20
|
-
require: [
|
|
24
|
+
require: [
|
|
25
|
+
path.join(__dirname, 'examples/theme.css'),
|
|
26
|
+
path.join(__dirname, 'vendor/doka/doka.min.css'),
|
|
27
|
+
],
|
|
21
28
|
sections: [
|
|
22
29
|
{
|
|
23
30
|
name: '',
|
|
@@ -52,6 +59,10 @@ module.exports = {
|
|
|
52
59
|
name: 'Accepting specific number of files',
|
|
53
60
|
content: 'examples/maxFiles/README.md'
|
|
54
61
|
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Custom validation',
|
|
64
|
+
content: 'examples/validator/README.md'
|
|
65
|
+
},
|
|
55
66
|
{
|
|
56
67
|
name: 'Opening File Dialog Programmatically',
|
|
57
68
|
content: 'examples/file-dialog/README.md'
|
|
@@ -69,6 +80,15 @@ module.exports = {
|
|
|
69
80
|
content: 'examples/plugins/README.md'
|
|
70
81
|
}
|
|
71
82
|
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'Integrations',
|
|
86
|
+
sections: [
|
|
87
|
+
{
|
|
88
|
+
name: 'Doka',
|
|
89
|
+
content: 'examples/doka/README.md'
|
|
90
|
+
}
|
|
91
|
+
]
|
|
72
92
|
}
|
|
73
93
|
]
|
|
74
94
|
}
|
|
@@ -10,7 +10,7 @@ export interface DropzoneProps extends DropzoneOptions {
|
|
|
10
10
|
|
|
11
11
|
export interface FileError {
|
|
12
12
|
message: string;
|
|
13
|
-
code: "file-too-large" | "file-too-small"|"too-many-files"|"file-invalid-type";
|
|
13
|
+
code: "file-too-large" | "file-too-small" | "too-many-files" | "file-invalid-type" | string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface FileRejection {
|
|
@@ -34,6 +34,7 @@ export type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, PropTypes> & {
|
|
|
34
34
|
onDropRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
|
35
35
|
getFilesFromEvent?: (event: DropEvent) => Promise<Array<File | DataTransferItem>>;
|
|
36
36
|
onFileDialogCancel?: () => void;
|
|
37
|
+
validator?: <T extends File>(file: T) => FileError | FileError[] | null;
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export type DropEvent = React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement> | DragEvent | Event;
|