├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── examples
└── basic
│ ├── client.js
│ ├── index.html
│ ├── index.js
│ ├── package.json
│ └── style.css
├── index.js
├── package.json
└── src
├── Receiver.js
├── UploadHandler.js
├── UploadManager.js
├── __tests__
├── Receiver-test.js
├── UploadHandler-test.js
└── UploadManager-test.js
├── constants
└── status.js
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ['es2015', 'react']
3 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "es6": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 6,
9 | "sourceType": "module",
10 | "ecmaFeatures": {
11 | "jsx": true
12 | }
13 | },
14 | "plugins": [
15 | "react"
16 | ],
17 | "extends": [
18 | "eslint:recommended",
19 | "plugin:react/recommended"
20 | ]
21 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | # IDE
30 | .idea
31 |
32 | # Transformed source
33 | lib
34 |
35 | # npmignore
36 | .npmignore
37 |
38 | # Examples
39 | examples/**/bundle.js
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # ES2015 source
2 | src
3 |
4 | # Test coverage
5 | coverage
6 |
7 | # Examples
8 | examples
9 |
10 | # Babel
11 | .babelrc
12 |
13 | # ESLint
14 | .eslintrc
15 |
16 | # IDE
17 | .idea
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 lionng429
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-file-uploader
2 |
3 | react-file-uploader is a set of customizable React components that helps you to build a file uploader in your application easily.
4 |
5 | It is expected to be production ready from v1.0.0, although v0.4.1 provides a stable but very simple and limited usage.
6 |
7 | The uploading implementation is coupled with [superagent](https://visionmedia.github.io/superagent/), and the `method`, `header` and `data` are configurable with props.
8 |
9 | ## Installation
10 |
11 | To install:
12 |
13 | ```sh
14 | npm install --save react-file-uploader
15 | ```
16 |
17 | # Documentation
18 |
19 | this module currently contains 4 major entities, which are
20 |
21 | 1. Receiver
22 | 2. UploadManager
23 | 3. UploadHandler
24 | 4. File Status
25 |
26 | # Receiver
27 |
28 | `Receiver` helps you to manage the **Drag and Drop** functionality. Once you mounted the `Receiver` component, your application will start listen to `dragenter`, `dragover`, `dragleave` and `drop` events.
29 |
30 | ```
31 | import { Receiver } from 'react-file-uploader';
32 |
33 |
43 |
44 | visual layer of the receiver (drag & drop panel)
45 |
46 |
47 | ```
48 |
49 | ## Props
50 |
51 | * wrapperId - `string`: Optional HTML element id for the DnD area. If not given, `window` will be used instead.
52 | * customClass - `string | array`: the class name(s) for the `div` wrapper
53 | * style - `object`: the style for the `div` wrapper
54 | * isOpen - `boolean` `required`: to control in the parent component whether the Receiver is visble.
55 | * onDragEnter - `function` `required`: when `isOpen` is `false`, this will be fired with the window event of `dragenter` once .
56 |
57 | You may make use of the drag & drop event callbacks.
58 |
59 | ```
60 | // @param e Object DragEvent
61 | function onDragEnter(e) {
62 | this.setState({ isReceiverOpen: true });
63 | }
64 | ```
65 |
66 | * onDragOver - `function`: this will be fired with the window event of `dragover`.
67 |
68 | ```
69 | // @param e Object DragEvent
70 | function onDragOver(e) {
71 | // your codes here
72 | }
73 | ```
74 |
75 | * onDragLeave - `function` `required`: when the drag event entirely left the client (i.e. `dragLevel === 0`), this will be fired with the window event of `dragleave` once.
76 |
77 | ```
78 | // @param e Object DragEvent
79 | function onDragLeave(e) {
80 | this.setState({ isReceiverOpen: false });
81 | }
82 | ```
83 |
84 | * onFileDrop - `function` `required`: this will be fired with the window event of `drop`. You may execute any validation / checking process here i.e. *size*, *file type*, etc.
85 |
86 | ```
87 | // @param e Object DragEvent
88 | // @param files Array the files dropped on the target node
89 | function onFileDrop(e, uploads) {
90 | // check if the files are drop on the targeted DOM
91 | const node = ReactDOM.findDOMNode(this.refs.uploadPanel);
92 | if (e.target !== node) {
93 | return;
94 | }
95 |
96 | let newUploads = uploads.map(upload => {
97 | // check file size
98 | if (upload.data.size > 1000 * 1000) {
99 | return Object.assign({}, upload, { error: 'file size exceeded 1MB' });
100 | }
101 | })
102 |
103 | // put files into state/stores by setState/action
104 | this.setState({
105 | uploads: this.state.uploads.concat(newUploads)),
106 | });
107 |
108 | // close the Receiver after file dropped
109 | this.setState({ isReceiverOpen: false });
110 | }
111 | ```
112 |
113 | # UploadManager
114 |
115 | Upload Manager serves as a high order component which helps you to manage the upload related parameters and functions. It prepares the upload function with [`superagent`](https://github.com/visionmedia/superagent) the children elements, and helps you to update the lifecycle status of the uploading files.
116 |
117 | ```
118 | import { UploadManager } from 'react-file-uploader';
119 |
120 |
135 | // UploadHandler as children
136 | {
137 | files.map(file => (
138 |
139 | ))
140 | }
141 |
142 | ```
143 |
144 | ## Props
145 |
146 | * component - `string`: the DOM tag name of the wrapper. By default it is an unordered list `ul`.
147 | * customClass - `string | array`: the class name(s) for the wrapper
148 | * formDataParser - **DEPRECATED** this prop function is renamed as `uploadDataHandler` starting from v1.0.0.
149 |
150 | * onUploadAbort - `function`: this will be fired when the upload request is aborted. This function is available from v1.0.0.
151 |
152 | ```
153 | /**
154 | * @param fileId {string} identifier of a file / an upload task
155 | * @param changes {object} changes object containing the new property of an upload
156 | * @param changes.status {number} file status ABORTED
157 | */
158 | let onUploadAbort = (fileId, { status }) => { ... }
159 | ```
160 |
161 | * onUploadStart - `function`: this will be fired when the upload request is just sent.
162 |
163 | ```
164 | /**
165 | * @param fileId {string} identifier of a file / an upload task
166 | * @param changes {object} changes object containing the new property of an upload
167 | * @param changes.status {number} file status UPLOADING
168 | */
169 | let onUploadStart = (fileId, { status }) => { ... }
170 | ```
171 |
172 | * onUploadProgress - `function`: this will be fired when the upload request returns progress. From v1.0.0, this callback is debounced for `props.progressDebounce` ms.
173 |
174 | ```
175 | /**
176 | * @param fileId {string} identifier of a file / an upload task
177 | * @param changes {object} changes object containing the new property of an upload
178 | * @param changes.progress {number} upload progress in percentage
179 | * @param changes.status {number} file status UPLOADING
180 | */
181 | let onUploadProgress = (fileId, { progress, status }) { ... }
182 | ```
183 |
184 | * onUploadEnd - `function` `required`: this will be fired upon the end of upload request.
185 |
186 | ```
187 | /**
188 | * @param fileId {string} identifier of a file / an upload task
189 | * @param changes {object} changes object containing the new property of an upload
190 | * @param changes.error {object} error returned from `props.uploadErrorHandler`
191 | * @param changes.progress {number} upload progress in percentage, either 0 or 100 with a corresponding FAILED or UPLOADED status
192 | * @param changes.result {object} upload result / response object returned from `props.uploadErrorHandler`
193 | * @param changes.status {number} file status, either FAILED or UPLOADED
194 | */
195 | // @param file Object the file object returned with either UPLOADED or FAILED status and 100% progress. When it is wilh FAILED status, error property should be also assigned to the file object.
196 | let onUploadEnd = (fileId, { error, progress, result, status }) => { ... }
197 | ```
198 |
199 | * progressDebounce - `number`: debounce value in ms for the callback on superagent `progress`.
200 | * reqConfigs - `object`: the exposed superagent configs including `accept`, `method`, `timeout` and `withCredentials`.
201 | * style - `object`: the style property for the wrapper.
202 | * uploadUrl - `string` `required`: the url of the upload end point from your server.
203 | * uploadDataHandler - `function`: this function is to parse the data to be sent as request data. From v1.0.0, the first argument will become a upload task object instead of the File instance.
204 |
205 | ```
206 | let uploadDataHandler = (upload) => {
207 | // for FormData
208 | const formData = new FormData();
209 | formData.append('file', upload.data);
210 | formData.append('custom-key', 'custom-value');
211 | return formData;
212 |
213 | // for AWS S3
214 | return upload.data;
215 | }
216 | ```
217 |
218 | * uploadErrorHandler - `function`: this function is to process the arguments of `(err, res)` in `superagent.end()`. In this function, you can resolve the error and result according to your upload api response. Default implementation is available as defaultProps.
219 |
220 | ```
221 | function uploadErrorHandler(err, res) {
222 | const body = res.body ? clone(res.body) : {};
223 | let error = null;
224 |
225 | if (err) {
226 | error = err.message;
227 | } else if (body.errors) {
228 | error = body.errors;
229 | }
230 |
231 | delete body.errors;
232 |
233 | return { error, result: body };
234 | }
235 | ```
236 |
237 | * uploadHeader - **DEPRECATED** this prop is deprecated and replaced by `uploadHeaderHandler`.
238 |
239 | * uploadHeaderHandler - `function`: the function is to parse the header object to be sent as request header.
240 |
241 | ```
242 | let uploadHeaderHandler = (upload) => {
243 | // for AWS S3
244 | return {
245 | 'Content-Type': upload.data.type,
246 | 'Content-Disposition': 'inline'
247 | };
248 | }
249 | ```
250 |
251 | # UploadHandler
252 |
253 | Upload Handler helps you to execute the upload lifecycle, which is `start`, `progress` and `end`. It also acts as the presentation layer of a file, showing users the info of the **_uploading / uploaded_** file.
254 |
255 | ```
256 | import { UploadHandler } from 'react-file-uploader';
257 |
258 |
265 | {
266 | // From v1.0.0, you can pass a render function as children, so to have access to the prepared `upload` and `abort` function.
267 | ({ upload, abort }) => (
268 |
269 | {file.data.name}
270 |
271 | {file.id}
272 | {file.data.type}
273 | {file.data.size / 1000 / 1000} MB
274 | {file.progress}%
275 |
276 | {this.getStatusString(file.status)}
277 |
278 | {file.error}
279 | {
280 | ((index % 2 === 1 && file.status === 0) || file.status === -2) && (
281 | Upload
282 | )
283 | }
284 | {
285 | file.status === 1 && (
286 | Abort
287 | )
288 | }
289 |
290 |
291 | )
292 | }
293 |
294 | ```
295 |
296 | ## Props
297 |
298 | * `abort` - `function` the function to abort the upload request. It is provided by UploadManager HOC by default.
299 | * `autoStart` - `boolean`: when `autoStart` is set, upon the UploadHandler `componentDidMount`, it will detect if the file i.e. *as props* is with the `PENDING` status and initialise an upload request which is sent to the `uploadUrl` you passed to the `UploadManager`.
300 | * `component` - `string`: the DOM tag name of the wrapper.
301 | * `customClass` - `string | array`: the class name(s) for the wrapper
302 | * `style` - `object`: the style for the wrapper
303 | * `file` - `object` `required`: the file object that is **_uploaded / going to be uploaded_**.
304 | * `upload` - `function`: the function that contains the upload logic, you may pass it directly when you are using `UploadHandler` alone, or it could be prepared by `UploadManager`.
305 |
306 | ```
307 | // @param url String API upload end point
308 | // @param file Object File Object
309 | let upload = (url, file) => { ... }
310 | ```
311 |
312 | # File Status
313 |
314 | `react-file-uploader` defines a set of status constants to represent the file status. The corresponding status will be assign to a file object throughout the uploading life cycle.
315 |
316 | ```
317 | ABORTED = -2
318 | FAILED = -1
319 | PENDING = 0
320 | UPLOADING = 1
321 | UPLOADED = 2
322 | ```
323 |
324 | # TODOs
325 |
326 | * complete test cases
327 | * add real-world example
328 | * verify and provide better support to Amazon Simple Storage Service
329 |
330 | # License
331 |
332 | MIT
--------------------------------------------------------------------------------
/examples/basic/client.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import * as FileUploader from '../../src/index';
4 |
5 | class MyComponent extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | isPanelOpen: false,
11 | isDragOver: false,
12 | files: [],
13 | };
14 |
15 | this.uploadPanel = undefined;
16 | this.openPanel = this.openPanel.bind(this);
17 | this.closePanel = this.closePanel.bind(this);
18 | this.onDragOver = this.onDragOver.bind(this);
19 | this.onFileDrop = this.onFileDrop.bind(this);
20 | this.onFileProgress = this.onFileProgress.bind(this);
21 | this.onFileUpdate = this.onFileUpdate.bind(this);
22 | this.setUploadPanelRef = this.setUploadPanelRef.bind(this);
23 | }
24 |
25 | openPanel() {
26 | this.setState({ isPanelOpen: true });
27 | }
28 |
29 | closePanel() {
30 | this.setState({ isPanelOpen: false });
31 | }
32 |
33 | onDragOver(e) {
34 | // your codes here:
35 | // if you want to check if the files are dragged over
36 | // a specific DOM node
37 | const node = ReactDOM.findDOMNode(this.uploadPanel);
38 |
39 | if (this.state.isDragOver !== (e.target === node)) {
40 | this.setState({
41 | isDragOver: e.target === node,
42 | });
43 | }
44 | }
45 |
46 | onFileDrop({ target }, files) {
47 | const node = ReactDOM.findDOMNode(this.uploadPanel);
48 |
49 | if (target !== node) {
50 | this.closePanel();
51 | return false;
52 | }
53 |
54 | const uploads = files.map((item = {}) => {
55 | if (item.data.size > 100 * 1000 * 1000) {
56 | return Object.assign({}, item, {
57 | status: FileUploader.status.FAILED,
58 | error: 'file size exceeded maximum',
59 | });
60 | }
61 |
62 | return item;
63 | });
64 |
65 | this.setState({
66 | files: this.state.files.concat(uploads),
67 | });
68 |
69 | // if you want to close the panel upon file drop
70 | this.closePanel();
71 | }
72 |
73 | onFileProgress(fileId, fileData) {
74 | const { files = [] } = this.state,
75 | newFiles = files.map(item => item.id === fileId ? Object.assign({}, item, fileData) : item);
76 |
77 | this.setState({
78 | files: newFiles,
79 | });
80 | }
81 |
82 | onFileUpdate(fileId, fileData) {
83 | const { files = [] } = this.state,
84 | newFiles = files.map(item => item.id === fileId ? Object.assign({}, item, fileData) : item);
85 |
86 | this.setState({
87 | files: newFiles,
88 | });
89 | }
90 |
91 | setUploadPanelRef(ref) {
92 | this.uploadPanel = ref;
93 | }
94 |
95 | getStatusString(status) {
96 | switch (status) {
97 | case -2:
98 | return 'aborted';
99 |
100 | case -1:
101 | return 'failed';
102 |
103 | case 0:
104 | return 'pending';
105 |
106 | case 1:
107 | return 'uploading';
108 |
109 | case 2:
110 | return 'uploaded';
111 |
112 | default:
113 | return '';
114 | }
115 | }
116 |
117 | render() {
118 | return (
119 |
120 |
{ this.props.title }
121 |
You can upload files with size with 1 MB at maximum
122 |
131 |
132 | {
133 | !this.state.isDragOver ? 'Drop here' : 'Files detected'
134 | }
135 |
136 |
137 |
138 |
Upload List
139 |
148 | {
149 | this.state.files.map((file, index) => (
150 |
151 | {
152 | ({ upload, abort }) => (
153 |
154 | {file.data.name}
155 |
156 | {file.id}
157 | {file.data.type}
158 | {file.data.size / 1000 / 1000} MB
159 | {file.progress}%
160 |
161 | {this.getStatusString(file.status)}
162 |
163 | {file.error}
164 | {
165 | ((index % 2 === 1 && file.status === 0) || file.status === -2) && (
166 | Upload
167 | )
168 | }
169 | {
170 | file.status === 1 && (
171 | Abort
172 | )
173 | }
174 |
175 |
176 | )
177 | }
178 |
179 | ))
180 | }
181 |
182 |
183 |
184 | );
185 | }
186 | }
187 |
188 | ReactDOM.render( , document.getElementById('app'));
189 |
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | react-file-uploader examples | basic
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/basic/index.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var express = require('express');
3 | var app = express();
4 | var multiparty = require('connect-multiparty')();
5 | var html = fs.readFileSync('index.html').toString();
6 |
7 | app.use(express.static(__dirname));
8 |
9 | app.get('/', function(req, res) {
10 | res.send(html);
11 | });
12 |
13 | app.post('/upload', multiparty, function(req, res) {
14 | var file = req.files.file;
15 |
16 | fs.unlink(file.path, function(err) {
17 | res.json({
18 | success: !err,
19 | file: file
20 | });
21 | });
22 | });
23 |
24 | app.listen(3000, function(err) {
25 | console.log('app is started at port 3000');
26 | });
--------------------------------------------------------------------------------
/examples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "test": "echo \"Error: no test specified\" && exit 1",
4 | "build": "browserify client.js -o bundle.js -t [ babelify --presets [ es2015 react ] ]"
5 | },
6 | "dependencies": {
7 | "connect-multiparty": "^2.0.0",
8 | "express": "^4.13.3",
9 | "forever": "^0.15.1",
10 | "react": "^16.0.0",
11 | "react-dom": "^16.0.0"
12 | },
13 | "devDependencies": {
14 | "babel-preset-es2015": "^6.1.18",
15 | "babel-preset-react": "^6.1.18",
16 | "babelify": "^7.2.0",
17 | "browserify": "^12.0.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/basic/style.css:
--------------------------------------------------------------------------------
1 | html, body, #app, #app > div {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
6 | .upload-panel {
7 | width: 400px;
8 | height: 200px;
9 | border: 2px dashed black;
10 | padding: 0 15px;
11 | }
12 |
13 | .upload-panel.hide {
14 | display: none;
15 | visibility: hidden;
16 | }
17 |
18 | .upload-list {
19 | width: 400px;
20 | height: auto;
21 | min-height: 200px;
22 | border: 2px dashed black;
23 | padding: 0 15px 15px;
24 | overflow: hidden;
25 | }
26 |
27 | .upload-list dd > span {
28 | display: block;
29 | float: left;
30 | width: 100%;
31 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('./lib/FileUploader');
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-file-uploader",
3 | "version": "1.0.0",
4 | "description": "A set of file-upload-components with React.js.",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "clean": "rm -rf lib",
8 | "test": "jest",
9 | "test:report": "jest --coverage",
10 | "build:lib": "babel src --out-dir lib",
11 | "build": "npm run eslint && npm run test && npm run clean && npm run build:lib",
12 | "eslint": "eslint ./src/*.js ./src/**/*.js",
13 | "prepublish": "npm run build"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/lionng429/react-file-uploader.git"
18 | },
19 | "keywords": [
20 | "react",
21 | "file",
22 | "upload",
23 | "uploader",
24 | "file-upload",
25 | "file-uploader"
26 | ],
27 | "author": "Marston Ng ",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/lionng429/react-file-uploader/issues"
31 | },
32 | "homepage": "https://github.com/lionng429/react-file-uploader",
33 | "devDependencies": {
34 | "babel": "^6.1.18",
35 | "babel-cli": "^6.2.0",
36 | "babel-jest": "*",
37 | "babel-preset-es2015": "^6.1.18",
38 | "babel-preset-react": "^6.1.18",
39 | "enzyme": "^3.3.0",
40 | "enzyme-adapter-react-16": "^1.1.1",
41 | "eslint": "^4.19.1",
42 | "eslint-plugin-react": "^7.7.0",
43 | "jest-cli": "*",
44 | "jsdom": "^7.0.2",
45 | "nock": "^8.0.0",
46 | "react-dom": "^15.0.0 || ^16.0.0"
47 | },
48 | "peerDependencies": {
49 | "react": "^15.0.0 || ^16.0.0"
50 | },
51 | "jest": {
52 | "scriptPreprocessor": "/node_modules/babel-jest",
53 | "testPathDirs": [
54 | "/src"
55 | ],
56 | "unmockedModulePathPatterns": [
57 | "/node_modules/react",
58 | "/node_modules/react-dom",
59 | "/node_modules/react-addons-test-utils",
60 | "/node_modules/jsdom",
61 | "/node_modules/lodash",
62 | "/node_modules/debug",
63 | "/node_modules/superagent",
64 | "/node_modules/nock"
65 | ]
66 | },
67 | "dependencies": {
68 | "classnames": "^2.2.0",
69 | "debug": "^2.2.0",
70 | "invariant": "^2.2.0",
71 | "lodash": ">=3.10.1",
72 | "prop-types": "^15.5.10",
73 | "react": "^15.0.0 || ^16.0.0",
74 | "shortid": "^2.2.6",
75 | "superagent": "^1.4.0"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Receiver.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import invariant from 'invariant';
4 | import classNames from 'classnames';
5 | import shortid from 'shortid';
6 | import status from './constants/status';
7 |
8 | class Receiver extends Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.wrapper = window;
13 | this.onDragEnter = this.onDragEnter.bind(this);
14 | this.onDragOver = this.onDragOver.bind(this);
15 | this.onDragLeave = this.onDragLeave.bind(this);
16 | this.onFileDrop = this.onFileDrop.bind(this);
17 |
18 | // this is to monitor the hierarchy
19 | // for window onDragEnter event
20 | this.state = {
21 | dragLevel: 0,
22 | };
23 | }
24 |
25 | componentDidMount() {
26 | invariant(
27 | (window.DragEvent || window.Event) && window.DataTransfer,
28 | 'Browser does not support DnD events or File API.'
29 | );
30 |
31 | const { wrapperId } = this.props;
32 |
33 | if (wrapperId) {
34 | const wrapperElement = document.getElementById(wrapperId);
35 |
36 | invariant(
37 | !!wrapperElement,
38 | `wrapper element with Id ${wrapperId} not found.`
39 | );
40 |
41 | this.wrapper = wrapperElement;
42 | }
43 |
44 | this.wrapper.addEventListener('dragenter', this.onDragEnter);
45 | this.wrapper.addEventListener('dragleave', this.onDragLeave);
46 | this.wrapper.addEventListener('dragover', this.onDragOver);
47 | this.wrapper.addEventListener('drop', this.onFileDrop);
48 | }
49 |
50 | componentWillReceiveProps(nextProps) {
51 | if (nextProps.wrapperId !== this.props.wrapperId) {
52 | // eslint-disable-next-line no-console
53 | console.warn('[Receiver.js] Change in props.wrapperId is unexpected, no new event listeners will be created.');
54 | }
55 | }
56 |
57 | componentWillUnmount() {
58 | this.wrapper.removeEventListener('dragenter', this.onDragEnter);
59 | this.wrapper.removeEventListener('dragleave', this.onDragLeave);
60 | this.wrapper.removeEventListener('dragover', this.onDragOver);
61 | this.wrapper.removeEventListener('drop', this.onFileDrop);
62 | }
63 |
64 | onDragEnter(e) {
65 | if (!e.dataTransfer.types.includes('Files')) {
66 | return;
67 | }
68 |
69 | const dragLevel = this.state.dragLevel + 1;
70 |
71 | this.setState({ dragLevel });
72 |
73 | if (!this.props.isOpen) {
74 | this.props.onDragEnter(e);
75 | }
76 | }
77 |
78 | onDragLeave(e) {
79 | const dragLevel = this.state.dragLevel - 1;
80 |
81 | this.setState({ dragLevel });
82 |
83 | if (dragLevel === 0) {
84 | this.props.onDragLeave(e);
85 | }
86 | }
87 |
88 | onDragOver(e) {
89 | e.preventDefault();
90 | this.props.onDragOver(e);
91 | }
92 |
93 | onFileDrop(e) {
94 | e.preventDefault();
95 |
96 | const uploads = [];
97 |
98 | if (e.dataTransfer && e.dataTransfer.files) {
99 | const fileList = e.dataTransfer.files;
100 |
101 | for (let i = 0; i < fileList.length; i++) {
102 | const upload = {
103 | id: shortid.generate(),
104 | status: status.PENDING,
105 | progress: 0,
106 | src: null,
107 | data: fileList[i]
108 | };
109 |
110 | uploads.push(upload);
111 | }
112 | }
113 |
114 | // reset drag level once dropped
115 | this.setState({ dragLevel: 0 });
116 |
117 | this.props.onFileDrop(e, uploads);
118 | }
119 |
120 | render() {
121 | const { isOpen, customClass, style, children } = this.props;
122 |
123 | return (
124 | isOpen ? (
125 |
126 | {children}
127 |
128 | ) : null
129 | );
130 | }
131 | }
132 |
133 | Receiver.propTypes = {
134 | children: PropTypes.oneOfType([
135 | PropTypes.element,
136 | PropTypes.arrayOf(PropTypes.element),
137 | ]),
138 | customClass: PropTypes.oneOfType([
139 | PropTypes.string,
140 | PropTypes.arrayOf(PropTypes.string),
141 | ]),
142 | isOpen: PropTypes.bool.isRequired,
143 | onDragEnter: PropTypes.func.isRequired,
144 | onDragOver: PropTypes.func,
145 | onDragLeave: PropTypes.func.isRequired,
146 | onFileDrop: PropTypes.func.isRequired,
147 | style: PropTypes.object,
148 | wrapperId: PropTypes.string,
149 | };
150 |
151 | Receiver.defaultProps = {
152 | isOpen: false
153 | };
154 |
155 | export default Receiver;
156 |
--------------------------------------------------------------------------------
/src/UploadHandler.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import invariant from 'invariant';
4 | import classNames from 'classnames';
5 | import uploadStatus from './constants/status';
6 |
7 | const debug = require('debug')('react-file-uploader:UploadHandler');
8 |
9 | class UploadHandler extends Component {
10 | componentDidMount() {
11 | const { file, upload, autoStart } = this.props;
12 |
13 | invariant(
14 | typeof upload === 'function',
15 | '`props.upload` must be a function'
16 | );
17 |
18 | invariant(
19 | !!file,
20 | '`props.file` must be provided'
21 | );
22 |
23 | if (file.status === uploadStatus.PENDING && autoStart) {
24 | debug('autoStart in on, calling upload()');
25 | upload(file);
26 | }
27 | }
28 |
29 | render() {
30 | const { abort, component, customClass, style, upload } = this.props;
31 |
32 | return React.createElement(
33 | component,
34 | { className: classNames(customClass), style },
35 | typeof this.props.children === 'function' ? this.props.children({ upload, abort }, this) : this.props.children
36 | );
37 | }
38 | }
39 |
40 | UploadHandler.propTypes = {
41 | abort: PropTypes.func.isRequired,
42 | autoStart: PropTypes.bool,
43 | children: PropTypes.oneOfType([
44 | PropTypes.element,
45 | PropTypes.arrayOf(PropTypes.element),
46 | PropTypes.func,
47 | ]),
48 | component: PropTypes.string.isRequired,
49 | customClass: PropTypes.oneOfType([
50 | PropTypes.string,
51 | PropTypes.arrayOf(PropTypes.string),
52 | ]),
53 | file: PropTypes.object.isRequired,
54 | style: PropTypes.object,
55 | upload: PropTypes.func.isRequired,
56 | };
57 |
58 | UploadHandler.defaultProps = {
59 | component: 'li',
60 | };
61 |
62 | export default UploadHandler;
63 |
--------------------------------------------------------------------------------
/src/UploadManager.js:
--------------------------------------------------------------------------------
1 | import React, { Component, cloneElement } from 'react';
2 | import PropTypes from 'prop-types';
3 | import invariant from 'invariant';
4 | import classNames from 'classnames';
5 | import bindKey from 'lodash/bindKey';
6 | import clone from 'lodash/clone';
7 | import debounce from 'lodash/debounce';
8 | import isEmpty from 'lodash/isEmpty';
9 | import superagent from 'superagent';
10 | import uploadStatus from './constants/status';
11 |
12 | const debug = require('debug')('react-file-uploader:UploadManager');
13 |
14 | class UploadManager extends Component {
15 | constructor(props) {
16 | super(props);
17 |
18 | this.requests = {};
19 | this.abort = this.abort.bind(this);
20 | this.upload = this.upload.bind(this);
21 | this.onProgress = debounce(this.onProgress.bind(this), props.progressDebounce);
22 | }
23 |
24 | componentDidMount() {
25 | // eslint-disable-next-line react/prop-types
26 | if (this.props.uploadHeader) {
27 | // eslint-disable-next-line no-console
28 | console.warn('`props.uploadHeader` is DEPRECATED. Please use `props.uploadHeaderHandler` instead.');
29 | }
30 |
31 | // eslint-disable-next-line react/prop-types
32 | if (this.props.formDataParser) {
33 | // eslint-disable-next-line no-console
34 | console.warn('`props.formDataParser` is DEPRECATED. Please use `props.uploadDataHandler` instead.');
35 | }
36 |
37 | invariant(
38 | !!this.props.uploadUrl,
39 | 'Upload end point must be provided to upload files'
40 | );
41 |
42 | invariant(
43 | !!this.props.onUploadEnd,
44 | 'onUploadEnd function must be provided'
45 | );
46 | }
47 |
48 | onProgress(fileId, progress) {
49 | const { onUploadProgress } = this.props,
50 | request = this.requests[fileId];
51 |
52 | if (request.xhr && request.xhr.readyState !== 4 && !request.aborted) {
53 | if (typeof onUploadProgress === 'function') {
54 | onUploadProgress(fileId, {
55 | progress,
56 | status: uploadStatus.UPLOADING,
57 | });
58 | }
59 | }
60 | }
61 |
62 | abort(file = {}) {
63 | const { onUploadAbort } = this.props,
64 | request = this.requests[file.id];
65 |
66 | if (!request) {
67 | debug('request instance not found.');
68 | return;
69 | }
70 |
71 | request.abort();
72 |
73 | if (typeof onUploadAbort === 'function') {
74 | onUploadAbort(file.id, { status: uploadStatus.ABORTED });
75 | }
76 | }
77 |
78 | upload(url, file) {
79 | const {
80 | reqConfigs: {
81 | accept = 'application/json',
82 | method = 'post',
83 | timeout,
84 | withCredentials = false,
85 | },
86 | onUploadStart,
87 | onUploadEnd,
88 | uploadDataHandler,
89 | uploadErrorHandler,
90 | uploadHeaderHandler,
91 | } = this.props;
92 |
93 | if (typeof onUploadStart === 'function') {
94 | onUploadStart(file.id, { status: uploadStatus.UPLOADING });
95 | }
96 |
97 | let header = uploadHeaderHandler(file),
98 | data = uploadDataHandler(file);
99 |
100 | let request = superagent[method.toLowerCase()](url)
101 | .accept(accept);
102 |
103 | if (!isEmpty(header)) {
104 | request.set(header);
105 | }
106 |
107 | if (timeout) {
108 | request.timeout(timeout);
109 | }
110 |
111 | if (withCredentials) {
112 | request.withCredentials();
113 | }
114 |
115 | this.requests[file.id] = request;
116 |
117 | debug(`start uploading file#${file.id} to ${url}`, file);
118 |
119 | request
120 | .send(data)
121 | .on('progress', ({ percent }) => this.onProgress(file.id, percent))
122 | .end((err, res) => {
123 | const { error, result } = uploadErrorHandler(err, res);
124 |
125 | if (error) {
126 | debug('failed to upload file', error);
127 | } else {
128 | debug('succeeded to upload file', result);
129 | }
130 |
131 | if (typeof onUploadEnd === 'function') {
132 | onUploadEnd(file.id, {
133 | progress: error && 0 || 100,
134 | error,
135 | result: error && undefined || result,
136 | status: error && uploadStatus.FAILED || uploadStatus.UPLOADED
137 | });
138 | }
139 | });
140 | }
141 |
142 | render() {
143 | const { component, customClass, style, children, uploadUrl } = this.props;
144 |
145 | return React.createElement(
146 | component,
147 | { className: classNames(customClass), style },
148 | React.Children.map(children, child => cloneElement(child, Object.assign({
149 | abort: bindKey(this, 'abort', child.props.file),
150 | upload: bindKey(this, 'upload', uploadUrl, child.props.file),
151 | }, child.props)))
152 | );
153 | }
154 | }
155 |
156 | UploadManager.propTypes = {
157 | children: PropTypes.oneOfType([
158 | PropTypes.element,
159 | PropTypes.arrayOf(PropTypes.element),
160 | ]),
161 | component: PropTypes.string.isRequired,
162 | customClass: PropTypes.oneOfType([
163 | PropTypes.string,
164 | PropTypes.arrayOf(PropTypes.string),
165 | ]),
166 | onUploadAbort: PropTypes.func,
167 | onUploadStart: PropTypes.func,
168 | onUploadProgress: PropTypes.func,
169 | onUploadEnd: PropTypes.func.isRequired,
170 | progressDebounce: PropTypes.number,
171 | reqConfigs: PropTypes.shape({
172 | accept: PropTypes.string,
173 | method: PropTypes.string,
174 | timeout: PropTypes.shape({
175 | response: PropTypes.number,
176 | deadline: PropTypes.number,
177 | }),
178 | withCredentials: PropTypes.bool,
179 | }),
180 | style: PropTypes.object,
181 | uploadDataHandler: PropTypes.func,
182 | uploadErrorHandler: PropTypes.func,
183 | uploadHeaderHandler: PropTypes.func,
184 | uploadUrl: PropTypes.string.isRequired,
185 | };
186 |
187 | UploadManager.defaultProps = {
188 | component: 'ul',
189 | progressDebounce: 150,
190 | reqConfigs: {},
191 | uploadDataHandler: (file) => {
192 | const formData = new FormData();
193 | formData.append('file', file.data);
194 | return formData;
195 | },
196 | uploadErrorHandler: (err, res = {}) => {
197 | const body = res.body ? clone(res.body) : {};
198 | let error = null;
199 |
200 | if (err) {
201 | error = err.message;
202 | } else if (body.errors) {
203 | error = body.errors;
204 | }
205 |
206 | delete body.errors;
207 |
208 | return { error, result: body };
209 | },
210 | uploadHeaderHandler: () => ({})
211 | };
212 |
213 | export default UploadManager;
214 |
--------------------------------------------------------------------------------
/src/__tests__/Receiver-test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef, max-len, no-console */
2 | jest.dontMock('../Receiver');
3 | jest.dontMock('../index');
4 | jest.dontMock('classnames');
5 |
6 | import React from 'react';
7 | import { mount, shallow, configure } from 'enzyme';
8 | import Adapter from 'enzyme-adapter-react-16';
9 | import { jsdom } from 'jsdom';
10 | import shortid from 'shortid';
11 |
12 | const FileUploader = require('../index');
13 | const uploadStatus = FileUploader.status;
14 | const Receiver = FileUploader.Receiver;
15 |
16 | configure({ adapter: new Adapter() });
17 |
18 | const testFile = {
19 | lastModified: 1465229147840,
20 | lastModifiedDate: 'Tue Jun 07 2016 00:05:47 GMT+0800 (HKT)',
21 | name: 'test.jpg',
22 | size: 1024,
23 | type: 'image/jpg',
24 | webkitRelativePath: '',
25 | };
26 |
27 | const testFileCopy = JSON.parse(JSON.stringify(testFile));
28 |
29 | const files = [testFile];
30 |
31 | const createEvent = (eventName) => {
32 | const event = document.createEvent('HTMLEvents');
33 | event.initEvent(eventName, false, true);
34 | event.preventDefault = jest.genMockFn();
35 | event.dataTransfer = {
36 | files,
37 | setData: jest.genMockFunction(),
38 | types: ['Files']
39 | };
40 |
41 | return event;
42 | };
43 |
44 | describe('Receiver', () => {
45 | let dragEnterEvent,
46 | dragOverEvent,
47 | dragLeaveEvent,
48 | dropEvent,
49 | stringClass = 'receiver',
50 | arrayClass = ['react', 'receiver'],
51 | customStyle = { display: 'block' };
52 |
53 | beforeEach(() => {
54 | global.document = jsdom();
55 | global.window = document.parentWindow;
56 | global.window.DragEvent = 'DragEvent';
57 | global.window.DataTransfer = 'DataTransfer';
58 |
59 | dragEnterEvent = createEvent('dragenter');
60 | dragOverEvent = createEvent('dragover');
61 | dragLeaveEvent = createEvent('dragleave');
62 | dropEvent = createEvent('drop');
63 | });
64 |
65 | describe('constructor()', () => {
66 | let emptyFn = () => {},
67 | component = (
68 |
74 | );
75 |
76 | beforeEach(() => {
77 | console.warn = jest.genMockFn();
78 | });
79 |
80 | afterEach(() => {
81 | console.warn.mockClear();
82 | });
83 |
84 | it('should throw an error if DnD or File API is not supported', () => {
85 | global.window.DragEvent = undefined;
86 | global.window.DataTransfer = undefined;
87 |
88 | expect(() => shallow(component)).toThrow('Browser does not support DnD events or File API.');
89 | });
90 |
91 | it('should assign window to this.wrapper if no wrapperId is provided', () => {
92 | const receiver = shallow(component);
93 | expect(receiver.instance().wrapper).toEqual(global.window);
94 | });
95 |
96 | it('should throw an error if wrapperId is given but element is not found', () => {
97 | expect(() => shallow(
98 |
105 | )).toThrow();
106 | });
107 |
108 | it('should not throw an error if wrapperId is given and the element exists', () => {
109 | expect(() => mount((
110 |
111 |
118 |
119 | ), { attachTo: document.body })).not.toThrow();
120 | });
121 |
122 | it('should console.warn when a new wrapperId is given', () => {
123 | const receiver = shallow(component);
124 | receiver.setProps({ wrapperId: 'newRandom' });
125 | expect(console.warn.mock.calls.length).toBe(1);
126 | });
127 | });
128 |
129 | describe('state of dragLevel', () => {
130 | let receiver,
131 | onDragEnter,
132 | onDragOver,
133 | onDragLeave,
134 | onFileDrop;
135 |
136 | beforeEach(() => {
137 | const mockOnDragEnter = jest.genMockFn();
138 | const mockOnDragOver = jest.genMockFn();
139 | const mockOnDragLeave = jest.genMockFn();
140 | const mockOnFileDrop = jest.genMockFn();
141 |
142 | onDragEnter = mockOnDragEnter;
143 | onDragOver = mockOnDragOver;
144 | onDragLeave = mockOnDragLeave;
145 | onFileDrop = mockOnFileDrop;
146 |
147 | const component = (
148 |
156 | );
157 |
158 | receiver = shallow(component);
159 | });
160 |
161 | it('should increase state of dragLevel by 1 with dragEnter event', () => {
162 | const oldDragLevel = receiver.state().dragLevel;
163 | window.dispatchEvent(dragEnterEvent);
164 | const newDragLevel = receiver.state().dragLevel;
165 | expect(newDragLevel).toEqual(oldDragLevel + 1);
166 | });
167 |
168 | it('should call onDragEnter with dragEnter event if isOpen is false', () => {
169 | window.dispatchEvent(dragEnterEvent);
170 | expect(onDragEnter).toBeCalled();
171 | });
172 |
173 | it('should not call onDragEnter with dragEnter event if isOpen is true', () => {
174 | receiver.setProps({ isOpen: true });
175 | window.dispatchEvent(dragEnterEvent);
176 | expect(onDragEnter).not.toBeCalled();
177 | });
178 |
179 | it('should not call onDragEnter callback with dragEnter event if dataTransfer.types does not include `Files`', () => {
180 | onDragEnter = jest.genMockFn();
181 | dragEnterEvent.dataTransfer.types = ['HTMLElement'];
182 |
183 | receiver.setProps({ onDragEnter });
184 |
185 | window.dispatchEvent(dragEnterEvent);
186 | expect(onDragEnter).not.toBeCalled();
187 | });
188 |
189 | it('should call event.preventDefault with dragOver event', () => {
190 | window.dispatchEvent(dragOverEvent);
191 | expect(dragOverEvent.preventDefault).toBeCalled();
192 | });
193 |
194 | it('should call onDragOver with dragOver event', () => {
195 | window.dispatchEvent(dragOverEvent);
196 | expect(onDragOver).toBeCalled();
197 | });
198 |
199 | it('should decrease state of dragLevel by 1 with dragLeave event', () => {
200 | const oldDragLevel = receiver.state().dragLevel;
201 | window.dispatchEvent(dragEnterEvent);
202 | const newDragLevel = receiver.state().dragLevel;
203 | expect(newDragLevel).toEqual(oldDragLevel + 1);
204 |
205 | window.dispatchEvent(dragLeaveEvent);
206 | const finalDragLevel = receiver.state().dragLevel;
207 | expect(finalDragLevel).toEqual(newDragLevel - 1);
208 | expect(onDragLeave).toBeCalled();
209 | });
210 |
211 | it('should call onDragLeave if state of dragLevel is not 0', () => {
212 | const oldDragLevel = receiver.state().dragLevel;
213 | window.dispatchEvent(dragEnterEvent);
214 | const newDragLevel = receiver.state().dragLevel;
215 | expect(newDragLevel).toEqual(oldDragLevel + 1);
216 |
217 | window.dispatchEvent(dragEnterEvent);
218 | const newerDragLevel = receiver.state().dragLevel;
219 | expect(newerDragLevel).toEqual(newDragLevel + 1);
220 |
221 | window.dispatchEvent(dragLeaveEvent);
222 | const finalDragLevel = receiver.state().dragLevel;
223 | expect(finalDragLevel).toEqual(newerDragLevel - 1);
224 | expect(onDragLeave).not.toBeCalled();
225 |
226 | window.dispatchEvent(dragLeaveEvent);
227 | const endDragLevel = receiver.state().dragLevel;
228 | expect(endDragLevel).toEqual(finalDragLevel - 1);
229 | expect(onDragLeave).toBeCalled();
230 | });
231 |
232 | it('should call event.preventDefault with drop event', () => {
233 | window.dispatchEvent(dropEvent);
234 | // eslint-disable-next-line no-undef
235 | expect(dropEvent.preventDefault).toBeCalled();
236 | });
237 |
238 | it('should call onFileDrop with drop event', () => {
239 | window.dispatchEvent(dropEvent);
240 | expect(onFileDrop).toBeCalled();
241 | });
242 |
243 | it('should set state of dragLevel to 0 with dragEnter event', () => {
244 | const oldDragLevel = receiver.state().dragLevel;
245 | window.dispatchEvent(dragEnterEvent);
246 | const newDragLevel = receiver.state().dragLevel;
247 | expect(newDragLevel).toEqual(oldDragLevel + 1);
248 |
249 | window.dispatchEvent(dropEvent);
250 | const finalDragLevel = receiver.state().dragLevel;
251 | expect(finalDragLevel).toEqual(0);
252 | });
253 |
254 | it('should not call any callback after Receiver did unmount', () => {
255 | receiver.unmount();
256 | window.dispatchEvent(dragEnterEvent);
257 | expect(onDragEnter).not.toBeCalled();
258 |
259 | window.dispatchEvent(dragOverEvent);
260 | expect(onDragOver).not.toBeCalled();
261 |
262 | window.dispatchEvent(dragLeaveEvent);
263 | expect(onDragLeave).not.toBeCalled();
264 |
265 | window.dispatchEvent(dropEvent);
266 | expect(onFileDrop).not.toBeCalled();
267 | });
268 | });
269 |
270 | describe('callbacks and callback arguments', () => {
271 | let fileId = 'Ghb19rg1',
272 | onDragEnter,
273 | onDragOver,
274 | onDragLeave,
275 | onFileDrop;
276 |
277 | beforeEach(() => {
278 | shortid.generate = jest.genMockFn().mockReturnValue(fileId);
279 |
280 | const mockOnDragEnter = (e) => {
281 | expect(e.type).toBe('dragenter');
282 | };
283 | const mockOnDragOver = (e) => {
284 | expect(e.type).toBe('dragover');
285 | };
286 | const mockOnDragLeave = (e) => {
287 | expect(e.type).toBe('dragleave');
288 | };
289 | const mockOnFileDrop = (e, _files) => {
290 | expect(e.type).toBe('drop');
291 | expect(shortid.generate).toBeCalled();
292 | const file = _files[0];
293 | expect(file.id).toBe(fileId);
294 | expect(file.status).toBe(uploadStatus.PENDING);
295 | expect(file.progress).toBe(0);
296 | expect(file.src).toBe(null);
297 | expect(file.data).toEqual(testFile);
298 | // to test data mutation
299 | expect(testFile).toEqual(testFileCopy);
300 | };
301 |
302 | onDragEnter = mockOnDragEnter;
303 | onDragOver = mockOnDragOver;
304 | onDragLeave = mockOnDragLeave;
305 | onFileDrop = mockOnFileDrop;
306 |
307 | const component = (
308 |
316 | );
317 |
318 | shallow(component);
319 | });
320 |
321 | afterEach(() => {
322 | shortid.generate.mockClear();
323 | });
324 |
325 | it('should execute the onDragEnter callback with a DragEvent with type `dragenter` as argument', () => {
326 | window.dispatchEvent(dragEnterEvent);
327 | });
328 |
329 | it('should execute the onDragOver callback with a DragEvent with type `dragover` as argument', () => {
330 | window.dispatchEvent(dragOverEvent);
331 | });
332 |
333 | it('should execute the onDragLeave callback with a DragEvent with type `dragleave` as argument', () => {
334 | window.dispatchEvent(dragLeaveEvent);
335 | });
336 |
337 | it('should execute the onFileDrop callback with a DragEvent with type `drop` as argument and it should not mutate the dataTransfer.files', () => {
338 | window.dispatchEvent(dropEvent);
339 | });
340 | });
341 |
342 | describe('render()', () => {
343 | let receiver,
344 | childrenItems = Array(5).fill().map((value, index) => ({index} ));
345 |
346 | beforeEach(() => {
347 | const mockOnDragEnter = jest.genMockFn();
348 | const mockOnDragOver = jest.genMockFn();
349 | const mockOnDragLeave = jest.genMockFn();
350 | const mockOnFileDrop = jest.genMockFn();
351 |
352 | const component = (
353 |
362 | {childrenItems}
363 |
364 | );
365 |
366 | receiver = shallow(component);
367 | });
368 |
369 | it('should render nothing if isOpen is false', () => {
370 | expect(receiver.type()).toEqual(null);
371 | expect(receiver.children().exists()).toBe(false);
372 | });
373 |
374 | it('should render a div wrapper with children if isOpen is true', () => {
375 | receiver.setProps({ isOpen: true });
376 | expect(receiver.type()).toEqual('div');
377 | expect(receiver.children().length).toEqual(childrenItems.length);
378 | });
379 |
380 | it('should render a div wrapper with customClass in string', () => {
381 | receiver.setProps({ isOpen: true, customClass: stringClass });
382 | expect(receiver.hasClass(stringClass)).toBe(true);
383 | });
384 |
385 | it('should render a div wrapper with customClass in array', () => {
386 | receiver.setProps({ isOpen: true, customClass: arrayClass });
387 | arrayClass.forEach((classname) => {
388 | expect(receiver.hasClass(classname)).toBe(true);
389 | });
390 | });
391 |
392 | it('should render a div wrapper with applying `props.style`', () => {
393 | receiver.setProps({ isOpen: true, style: customStyle });
394 | expect(receiver.prop('style')).toEqual(customStyle);
395 | });
396 | });
397 | });
398 | /* eslint-enable no-undef, max-len, no-console */
399 |
--------------------------------------------------------------------------------
/src/__tests__/UploadHandler-test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef, max-len, no-console */
2 | jest.dontMock('../UploadHandler');
3 | jest.dontMock('../index');
4 | jest.dontMock('classnames');
5 |
6 | import React from 'react';
7 | import { shallow, configure } from 'enzyme';
8 | import Adapter from 'enzyme-adapter-react-16';
9 |
10 | const FileUploader = require('../index');
11 | const uploadStatus = FileUploader.status;
12 | const UploadHandler = FileUploader.UploadHandler;
13 |
14 | configure({ adapter: new Adapter() });
15 |
16 | describe('UploadHandler', () => {
17 | let uploadHandler,
18 | component,
19 | mockAbort,
20 | mockUpload,
21 | stringClass = 'receiver',
22 | arrayClass = ['react', 'receiver'],
23 | customStyle = { display: 'block' },
24 | children = (children ),
25 | renderFunction = jest.genMockFn(),
26 | file = { id: 'fileId', status: uploadStatus.PENDING };
27 |
28 | beforeEach(() => {
29 | mockAbort = jest.genMockFn();
30 | mockUpload = jest.genMockFn();
31 | renderFunction.mockReturnValue(children);
32 |
33 | component = (
34 |
41 | );
42 |
43 | uploadHandler = shallow(component);
44 | });
45 |
46 | describe('componentDidMount()', () => {
47 | it('should throw an error if `props.upload` is not a function', () => {
48 | expect(() => shallow(
49 |
53 | )).toThrow('`props.upload` must be a function');
54 | });
55 |
56 | it('should throw an error if `props.file` is missing', () => {
57 | expect(() => shallow(
58 |
62 | )).toThrow('`props.file` must be provided');
63 | });
64 |
65 | it('should call `props.upload()` if `props.autoStart` is true', () => {
66 | uploadHandler = shallow(
67 |
72 | );
73 |
74 | expect(mockUpload).toBeCalledWith(file);
75 | });
76 | });
77 |
78 | describe('render()', () => {
79 | it('should render a HTML `props.component` element as wrapper', () => {
80 | expect(uploadHandler.type()).toEqual('li');
81 | uploadHandler.setProps({ component: 'div' });
82 | expect(uploadHandler.type()).toEqual('div');
83 | expect(uploadHandler.children().exists()).toBe(false);
84 | });
85 |
86 | it('should render ReactElement children if it is given', () => {
87 | uploadHandler.setProps({ children });
88 | expect(uploadHandler.children().matchesElement(children));
89 | });
90 |
91 | it('should accept children as render function with { abort, upload } and the instance itself', () => {
92 | uploadHandler.setProps({ children: renderFunction });
93 | expect(renderFunction).toBeCalledWith({ abort: mockAbort, upload: mockUpload }, uploadHandler.instance());
94 | expect(uploadHandler.children().matchesElement(children));
95 | });
96 |
97 | it('should render a div wrapper with customClass in string', () => {
98 | uploadHandler.setProps({ customClass: stringClass });
99 | expect(uploadHandler.hasClass(stringClass)).toBe(true);
100 | });
101 |
102 | it('should render a div wrapper with customClass in array', () => {
103 | uploadHandler.setProps({ customClass: arrayClass });
104 | arrayClass.forEach((classname) => {
105 | expect(uploadHandler.hasClass(classname)).toBe(true);
106 | });
107 | });
108 |
109 | it('should render a div wrapper with applying `props.style`', () => {
110 | uploadHandler.setProps({ style: customStyle });
111 | expect(uploadHandler.prop('style')).toEqual(customStyle);
112 | });
113 | });
114 | });
115 | /* eslint-enable no-undef, max-len, no-console */
116 |
--------------------------------------------------------------------------------
/src/__tests__/UploadManager-test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef, max-len */
2 | jest.dontMock('../UploadManager');
3 | jest.dontMock('../index');
4 | jest.dontMock('classnames');
5 | jest.dontMock('lodash');
6 |
7 | import React from 'react';
8 | import { shallow, configure } from 'enzyme';
9 | import Adapter from 'enzyme-adapter-react-16';
10 | import { jsdom } from 'jsdom';
11 | import nock from 'nock';
12 |
13 | const FileUploader = require('../index');
14 | const UploadManager = FileUploader.UploadManager;
15 | const uploadStatus = FileUploader.status;
16 |
17 | configure({ adapter: new Adapter() });
18 |
19 | describe('UploadManager', () => {
20 | let stringClass = 'receiver',
21 | arrayClass = ['react', 'receiver'],
22 | uploadPath = 'http://localhost:3000/api/upload',
23 | timeout = {
24 | response: 1000,
25 | deadline: 1000,
26 | },
27 | children = (children
),
28 | uploadManager,
29 | onUploadAbort,
30 | onUploadStart,
31 | onUploadProgress,
32 | onUploadEnd,
33 | uploadDataHandler,
34 | uploadHeaderHandler,
35 | err,
36 | errorResponse,
37 | successResponse,
38 | errorHandler,
39 | file,
40 | fileCopy,
41 | customHeader;
42 |
43 | beforeEach(() => {
44 | global.document = jsdom();
45 | global.window = document.parentWindow;
46 |
47 | customHeader = {
48 | 'Accept': 'customAccept',
49 | 'Content-Type': 'customContentType',
50 | 'Content-Disposition': 'customContentDisposition'
51 | };
52 |
53 | onUploadAbort = jest.genMockFn();
54 | onUploadStart = jest.genMockFn();
55 | onUploadProgress = jest.genMockFn();
56 | onUploadEnd = jest.genMockFn();
57 | uploadDataHandler = jest.genMockFn();
58 | uploadHeaderHandler = jest.genMockFn().mockReturnValue(customHeader);
59 |
60 | file = { id: 'fileId' };
61 | fileCopy = JSON.parse(JSON.stringify(file));
62 |
63 | err = new Error('not found');
64 | errorResponse = { body: { success: false, errors: { message: 'not found' } } };
65 | successResponse = { body: { success: true } };
66 | errorHandler = UploadManager.defaultProps.uploadErrorHandler;
67 |
68 | nock('http://localhost:3000')
69 | .filteringRequestBody(() => '*')
70 | .post('/api/upload', '*')
71 | .reply(200, successResponse);
72 |
73 | uploadManager = shallow(
74 |
90 | {children}
91 |
92 | )
93 | });
94 |
95 | afterEach(() => {
96 | nock.cleanAll();
97 | nock.enableNetConnect();
98 | });
99 |
100 | describe('render()', () => {
101 | it('should render ul element by default', () => {
102 | expect(uploadManager.type()).toEqual('ul');
103 | expect(uploadManager.childAt(0).type()).toEqual('p');
104 | });
105 |
106 | it('should render wrapper element according to component props', () => {
107 | uploadManager.setProps({ component: 'div' });
108 | expect(uploadManager.type()).toEqual('div');
109 | });
110 |
111 | it('should render a wrapper with customClass in string', () => {
112 | expect(uploadManager.hasClass(stringClass)).toBe(true);
113 | });
114 |
115 | it('should render a wrapper with customClass in array', () => {
116 | uploadManager.setProps({ customClass: arrayClass });
117 |
118 | arrayClass.forEach((classname) => {
119 | expect(uploadManager.hasClass(classname)).toBe(true);
120 | });
121 | });
122 | });
123 |
124 | describe('uploadDataHandler()', () => {
125 | it('should return a FileData instance with a file data set', () => {
126 | const file = { data: 'fileData' },
127 | result = UploadManager.defaultProps.uploadDataHandler(file);
128 | expect(result).toBeInstanceOf(FormData);
129 | expect(result.get('file')).toEqual(file.data);
130 | });
131 | });
132 |
133 | describe('uploadErrorHandler()', () => {
134 | it('should return an object contains key of `error` and `result`', () => {
135 | const result = errorHandler(null, successResponse);
136 | expect(result.error).toBeNull();
137 | expect(result.result).toEqual(successResponse.body);
138 | });
139 |
140 | it('should return an object with key of `error` with value equals to the first argument if it is not empty', () => {
141 | const result = errorHandler(err, successResponse);
142 | expect(result.error).toEqual(err.message);
143 | expect(result.result).toEqual(successResponse.body);
144 | });
145 |
146 | it('should return an object with key of `error` with value equals to the value of `body.error` of the second argument if it is not empty', () => {
147 | const result = errorHandler(null, errorResponse);
148 | expect(result.error).toEqual(errorResponse.body.errors);
149 | delete errorResponse.body.errors;
150 | expect(result.result).toEqual(errorResponse.body);
151 | });
152 | });
153 |
154 | describe('uploadHeaderHandler()', () => {
155 | it('should return an empty object', () => {
156 | expect(UploadManager.defaultProps.uploadHeaderHandler()).toEqual({});
157 | });
158 | });
159 |
160 | describe('upload()', () => {
161 | it('should declare the request instance', () => {
162 | const instance = uploadManager.instance();
163 | instance.upload(instance.props.uploadUrl, file);
164 |
165 | const request = instance.requests[file.id];
166 | expect(request._timeout).toEqual(timeout);
167 | expect(request._header.accept).toEqual(customHeader['Accept']);
168 | expect(request._header['content-type']).toEqual(customHeader['Content-Type']);
169 | expect(request._header['content-disposition']).toEqual(customHeader['Content-Disposition']);
170 | });
171 |
172 | it('should call `props.onUploadStart` function if it is given', () => {
173 | const instance = uploadManager.instance();
174 | instance.upload(instance.props.uploadUrl, file);
175 | expect(onUploadStart).toBeCalledWith(file.id, { status: uploadStatus.UPLOADING });
176 | expect(file).toEqual(fileCopy);
177 | });
178 |
179 | it('should call `props.uploadDataHandler` function if it is given', () => {
180 | const instance = uploadManager.instance(),
181 | file = {};
182 | instance.upload(instance.props.uploadUrl, file);
183 | expect(uploadDataHandler).toBeCalledWith(file);
184 | });
185 |
186 | it('should call `props.uploadHeaderHandler` function if it is given', () => {
187 | const instance = uploadManager.instance(),
188 | file = {};
189 | instance.upload(instance.props.uploadUrl, file);
190 | expect(uploadHeaderHandler).toBeCalledWith(file);
191 | });
192 | });
193 |
194 | describe('abort()', () => {
195 | let instance, request;
196 |
197 | beforeEach(() => {
198 | instance = uploadManager.instance();
199 | instance.upload(instance.props.uploadUrl, file);
200 | request = instance.requests[file.id];
201 | request.abort = jest.genMockFn();
202 | });
203 |
204 | afterEach(() => {
205 | request.abort.mockClear();
206 | });
207 |
208 | it('should call `request.abort()` and `props.onUploadAbort()` if request instance is found.', () => {
209 | instance.abort();
210 | expect(request.abort).not.toBeCalled();
211 |
212 | instance.abort(file);
213 | expect(request.abort).toBeCalled();
214 | expect(onUploadAbort).toBeCalledWith(file.id, { status: uploadStatus.ABORTED });
215 | });
216 | });
217 |
218 | describe('onProgress()', () => {
219 | let instance, request, progress = 10;
220 |
221 | beforeEach(() => {
222 | instance = uploadManager.instance();
223 | instance.upload(instance.props.uploadUrl, file);
224 | request = instance.requests[file.id];
225 | request.aborted = false;
226 | request.xhr = {};
227 | });
228 |
229 | it('should call `props.onUploadProgress()` if request is not aborted', (done) => {
230 | instance.onProgress(file.id, progress);
231 | setTimeout(() => {
232 | expect(onUploadProgress).toBeCalledWith(file.id, { progress, status: uploadStatus.UPLOADING });
233 | done();
234 | }, instance.props.progressDebounce);
235 | });
236 | });
237 | });
238 | /* eslint-enable no-undef, max-len */
239 |
--------------------------------------------------------------------------------
/src/constants/status.js:
--------------------------------------------------------------------------------
1 | export default {
2 | ABORTED: -2,
3 | FAILED: -1,
4 | PENDING: 0,
5 | UPLOADING: 1,
6 | UPLOADED: 2,
7 | };
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Receiver from './Receiver';
2 | import UploadManager from './UploadManager';
3 | import UploadHandler from './UploadHandler';
4 | import status from './constants/status';
5 |
6 | export {
7 | Receiver,
8 | UploadManager,
9 | UploadHandler,
10 | status,
11 | };
12 |
13 |
--------------------------------------------------------------------------------