57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 |
66 | };
67 |
68 | ImageProgress.propTypes = {
69 | src: PropTypes.string.isRequired,
70 | progress: PropTypes.number.isRequired,
71 | completed: PropTypes.bool.isRequired
72 | };
73 |
74 | export default ImageProgress;
--------------------------------------------------------------------------------
/examples/components/ImageResponse.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Image} from 'cloudinary-react';
3 |
4 | import CircularProgress from "@material-ui/core/es/CircularProgress/CircularProgress";
5 |
6 | class ImageResponse extends React.Component {
7 |
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | loaded: false,
12 | }
13 | };
14 |
15 | render() {
16 |
17 | const imageStyle = {
18 | transition: 'all 0.5s',
19 | width: 'inherit',
20 | height: 'inherit',
21 | };
22 |
23 | const frontStyle = {
24 | backfaceVisibility: 'hidden',
25 | height: 'inherit',
26 | width: 'inherit',
27 | position: 'absolute',
28 | zIndex: 20,
29 | display: 'flex',
30 | justifyContent: 'center',
31 | alignItems: 'center',
32 | backgroundColor: 'whitesmoke',
33 | };
34 |
35 | const backStyle = {
36 | transform: 'rotateY(180deg)',
37 | height: 'inherit',
38 | width: 'inherit',
39 | position: 'absolute',
40 | zIndex: 10,
41 | };
42 |
43 | const progressCtrStyle = {
44 | transform: `rotateY(${this.state.loaded ? '180' : 0}deg)`,
45 | transition: 'all 0.5s linear',
46 | transformStyle: 'preserve-3d',
47 | height: 'inherit',
48 | width: 'inherit',
49 | };
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 | {
61 | this.setState({loaded: true})
62 | }}
63 | />
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | export default ImageResponse;
--------------------------------------------------------------------------------
/examples/components/ImageUploadDemo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from '@material-ui/core/Button';
4 | import Icon from "@material-ui/core/es/Icon/Icon";
5 | import Badge from "@material-ui/core/es/Badge/Badge";
6 | import Switch from "@material-ui/core/es/Switch/Switch";
7 | import Paper from "@material-ui/core/es/Paper/Paper";
8 | import Table from "@material-ui/core/es/Table/Table";
9 | import TableHead from "@material-ui/core/es/TableHead/TableHead";
10 | import TableRow from "@material-ui/core/es/TableRow/TableRow";
11 | import TableCell from "@material-ui/core/es/TableCell/TableCell";
12 | import TableBody from "@material-ui/core/es/TableBody/TableBody";
13 | import LinearProgress from "@material-ui/core/es/LinearProgress/LinearProgress";
14 | import CircularProgress from "@material-ui/core/es/CircularProgress/CircularProgress";
15 | import FormControlLabel from "@material-ui/core/es/FormControlLabel/FormControlLabel";
16 |
17 | import ImagePreview from "./ImagePreview";
18 | import ImageProgress from "./ImageProgress";
19 | import ImageResponse from "./ImageResponse";
20 | import SelectFileButton from "./SelectFileButton";
21 | import FileManager from '../../src/components/FileManager';
22 | import FileUploader from '../../src/components/FileUploader';
23 |
24 | const CLOUD_NAME = 'dpdenton';
25 | const CLOUD_URL = `https://api.cloudinary.com/v1_1/${CLOUD_NAME}/upload`;
26 |
27 | const styles = {
28 |
29 | containerStyle: {
30 | border: 'thin solid rgb(221, 221, 221)',
31 | display: 'flex',
32 | flexWrap: 'wrap',
33 | justifyContent: 'center',
34 | },
35 | controlsStyle: {
36 | padding: 25,
37 | backgroundColor: 'whitesmoke',
38 | textAlign: 'center',
39 | },
40 | controlStyle: {
41 | margin: 10,
42 |
43 | },
44 | fileStyle: {
45 | width: '100%',
46 | height: '100%',
47 | border: 'thin solid #eee',
48 | borderRadius: 10,
49 | overflow: 'hidden',
50 | cursor: 'pointer',
51 | },
52 | wrapperStyle: {
53 | position: 'relative',
54 | },
55 |
56 | buttonStyle: {
57 | position: 'absolute',
58 | width: 44,
59 | height: 44,
60 | right: 16,
61 | bottom: 16,
62 | },
63 |
64 | progressStyle: {
65 | position: 'absolute',
66 | width: 52,
67 | height: 52,
68 | bottom: 12,
69 | right: 12,
70 | zIndex: 1,
71 | color: 'white',
72 | },
73 |
74 | eventContainer: {
75 | padding: 25,
76 | backgroundColor: 'whitesmoke'
77 | }
78 | };
79 |
80 | class ImageUploadDemo extends React.Component {
81 |
82 | constructor(props) {
83 | super(props);
84 | this.state = {
85 | files: [],
86 | events: {},
87 | progress: {},
88 | selectedIndex: null,
89 | multiple: true,
90 | autoUpload: false,
91 | showEvents: false,
92 | };
93 |
94 | this.uploadFile = this.uploadFile.bind(this);
95 | }
96 |
97 | render() {
98 |
99 | const totalProgress = Object.values(this.state.progress).reduce((a, b) => a + b, 0);
100 | const progress = totalProgress / Object.keys(this.state.progress).length * 100 || 0;
101 |
102 | return (
103 |
104 |
108 |
109 |
110 |
111 | {
114 | this.setState({files: this.state.files.concat(Array.from(event.target.files))})
115 | }}
116 | button={(
117 |
126 | )}
127 | />
128 |
129 |
130 |
this.setState({multiple: !this.state.multiple})}
137 | />
138 | }
139 | label="Allow Multiple"
140 | />
141 | this.setState({autoUpload: !this.state.autoUpload})}
148 | />
149 | }
150 | label="Auto Upload"
151 | />
152 | this.setState({showEvents: !this.state.showEvents})}
159 | />
160 | }
161 | label="Show Events"
162 | />
163 |
164 |
165 |
168 | {files => {files.map(this.uploadFile)}
}
169 |
170 |
171 | {this.state.showEvents
172 | && this.selectedIndex !== null
173 | && Object.keys(this.state.events).length > 0
174 | && this.renderEvents()}
175 |
176 | );
177 | }
178 |
179 | uploadFile(file) {
180 |
181 | return (
182 |
{
194 | const {progress} = this.state;
195 | progress[file.key] = 0;
196 | this.setState({progress, selectedIndex: file.key});
197 | this.addTransitionState(event, FileUploader.UPLOAD_READY, file.key);
198 | }}
199 | onUploadStart={event => {
200 | this.addTransitionState(event, FileUploader.UPLOAD_START, file.key);
201 | }}
202 | onUploadProgress={event => {
203 | const {progress} = this.state;
204 | progress[file.key] = event.total ? event.loaded / event.total : 0;
205 | this.setState({progress});
206 | this.addTransitionState(event, FileUploader.UPLOAD_PROGRESS, file.key);
207 | }}
208 | onUploadComplete={event => {
209 | this.addTransitionState(event, FileUploader.UPLOAD_COMPLETE, file.key);
210 | }}
211 | onDownloadStart={event => {
212 | this.addTransitionState(event, FileUploader.DOWNLOAD_START, file.key);
213 | }}
214 | onDownloadProgress={event => {
215 | this.addTransitionState(event, FileUploader.DOWNLOAD_PROGRESS, file.key);
216 | }}
217 | onDownloadComplete={event => {
218 | const {progress} = this.state;
219 | delete progress[file.key];
220 | this.setState({progress});
221 | this.addTransitionState(event, FileUploader.DOWNLOAD_COMPLETE, file.key);
222 | }}
223 | >{data => {
224 |
225 | const fileContainerStyle = {
226 | width: 150,
227 | height: 150,
228 | margin: 25,
229 | };
230 |
231 | return (
232 | {
235 | this.setState({selectedIndex: file.key});
236 | }}
237 | >
238 |
239 | {ImageUploadDemo.renderImage(data)}
240 |
241 |
242 | {ImageUploadDemo.renderButton(data)}
243 |
244 |
245 |
246 | );
247 | }}
248 |
249 | )
250 | };
251 |
252 | static renderButton(data) {
253 |
254 | switch (data.requestState) {
255 |
256 | case FileUploader.UPLOAD_READY:
257 | return (
258 |
259 |
267 |
268 | );
269 |
270 | case FileUploader.ABORT:
271 | case FileUploader.UPLOAD_START:
272 | case FileUploader.UPLOAD_PROGRESS: {
273 |
274 | const progress = data.uploadProgress
275 | ? Math.floor(data.uploadProgress.loaded / data.uploadProgress.total * 100)
276 | : 0;
277 |
278 | return (
279 |
280 |
288 |
292 |
293 | );
294 | }
295 |
296 | case FileUploader.UPLOAD_COMPLETE:
297 | case FileUploader.DOWNLOAD_PROGRESS:
298 | case FileUploader.DOWNLOAD_COMPLETE:
299 | return (
300 |
301 |
308 |
309 | );
310 |
311 | case FileUploader.ERROR:
312 | return
Error
;
313 |
314 | default:
315 | return
Something has gone wrong!
;
316 | }
317 |
318 | }
319 |
320 | static renderImage(data) {
321 |
322 | switch (data.requestState) {
323 |
324 | case FileUploader.UPLOAD_READY:
325 | return (
326 |
329 | );
330 |
331 | case FileUploader.UPLOAD_START:
332 | case FileUploader.UPLOAD_PROGRESS:
333 | case FileUploader.UPLOAD_COMPLETE:
334 | case FileUploader.ABORT: {
335 |
336 | const progress = data.uploadProgress
337 | ? Math.floor(data.uploadProgress.loaded / data.uploadProgress.total * 100)
338 | : 0;
339 |
340 | return (
341 |
346 | );
347 | }
348 |
349 | case FileUploader.DOWNLOAD_PROGRESS:
350 | case FileUploader.DOWNLOAD_COMPLETE:
351 | return (
352 |
361 | );
362 | default:
363 | return
Something has gone wrong!
;
364 | }
365 | }
366 |
367 | renderEvents() {
368 |
369 | return (
370 |
371 |
372 |
373 |
374 |
375 | Event Name
376 | Event Object
377 |
378 |
379 |
380 | {this.state.events[this.state.selectedIndex]
381 | .filter(event => event.eventName !== null)
382 | .reduce((events, event) => {
383 | const existingEvent = events.find(t => t.eventName === event.eventName);
384 | if (existingEvent) {
385 | existingEvent.count += 1;
386 | } else {
387 | events.push({
388 | count: 1,
389 | ...event,
390 | });
391 | }
392 | return events;
393 | }, [])
394 | .map(event => (
395 |
396 |
397 | {}
402 | {event.eventName}
403 |
404 |
405 | {event.eventObject.constructor.name}
406 |
407 |
408 | )
409 | )}
410 |
411 |
412 |
413 |
414 | )
415 | }
416 |
417 | addTransitionState(event, eventName, index) {
418 |
419 | const {events} = this.state;
420 |
421 | const eventState = {
422 | eventName,
423 | eventObject: event,
424 | eventTimestamp: +new Date(),
425 | };
426 |
427 | if (!events[index]) {
428 | events[index] = [eventState];
429 | } else {
430 | events[index].push(eventState);
431 | }
432 |
433 | this.setState({events})
434 | }
435 | }
436 |
437 | export default ImageUploadDemo;
438 |
--------------------------------------------------------------------------------
/examples/components/SelectFileButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const SelectFileButton = props => (
5 |
6 |
7 |
15 | {
16 | global.document.getElementById('___SelectFileButton___').click();
17 | }}>
18 | {props.button}
19 |
20 |
21 | );
22 |
23 | SelectFileButton.propTypes = {
24 | onChange: PropTypes.func.isRequired,
25 | button: PropTypes.node,
26 | multiple: PropTypes.bool,
27 | };
28 |
29 | SelectFileButton.defaultProps = {
30 | button:
,
31 | multiple: true,
32 | };
33 |
34 | export default SelectFileButton;
--------------------------------------------------------------------------------
/examples/components/Vanilla.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FileManager from "../../src/components/FileManager";
3 | import FileUploader from "../../src/components/FileUploader";
4 |
5 | class Vanilla extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | files: [],
11 | };
12 | this.uploadFiles = this.uploadFiles.bind(this);
13 | this.uploadFile = this.uploadFile.bind(this);
14 | }
15 |
16 | render() {
17 | return (
18 |
19 | this.setState({files: this.state.files.concat(Array.from(event.target.files))})}
23 | multiple
24 | />
25 | {this.uploadFiles}
28 |
29 |
30 | )
31 | }
32 |
33 | uploadFiles(files) {
34 | return files.map(this.uploadFile);
35 | }
36 |
37 | uploadFile(file) {
38 | return (
39 |
50 | {this.fileProgress}
51 |
52 | )
53 | }
54 |
55 | static fileProgress({
56 |
57 | /*
58 | References to the Event objects.
59 | Initial state is null and gets assign on each Event.
60 | */
61 | uploadReady,
62 | uploadStart,
63 | uploadProgress,
64 | uploadComplete,
65 | downloadStart,
66 | downloadProgress,
67 | downloadComplete,
68 | error,
69 | abort,
70 | timeout,
71 |
72 | /*
73 | The sequential state of the request
74 | enum {
75 | uploadReady, uploadStart, uploadProgress, uploadComplete, downloadStart
76 | downloadStart, downloadProgress, downloadComplete
77 | }
78 | */
79 | requestState, //
80 |
81 | /*
82 | Function references to start / abort request
83 | */
84 | startUpload,
85 | abortRequest,
86 |
87 | /*
88 | Request Object reference (XMLHttpReqeust)
89 | */
90 | request,
91 |
92 | /*
93 | Response text Object (JSON)
94 | */
95 | response,
96 |
97 | /*
98 | Data of the file being uploaded (if readData props is true)
99 | */
100 | fileData,
101 |
102 | }) {
103 | return (
104 |
105 | {fileData &&

}
106 | {startUpload &&
}
107 | {requestState && requestState}
108 |
109 | )
110 | }
111 |
112 | }
113 |
114 | export default Vanilla;
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
ReactJS File Uploader
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {AppContainer} from 'react-hot-loader'; // eslint-disable-line import/no-extraneous-dependencies
4 |
5 | import App from './components/App';
6 |
7 | const rootEl = global.document.getElementById('app');
8 | const render = AppComponent => {
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | rootEl
14 | );
15 | };
16 |
17 | /* eslint-disable global-require, import/newline-after-import */
18 | render(App);
19 | if (module.hot)
20 | module.hot.accept(App, () => render(require('./components/App').default));
21 | /* eslint-enable global-require, import/newline-after-import */
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactjs-file-uploader",
3 | "version": "0.1.7",
4 | "description": "Flexible React file uploader supporting progress feedback, multiple images and upload/abort controls.",
5 | "scripts": {
6 | "build": "npm run clean && cross-env NODE_ENV=production TARGET=umd webpack --bail",
7 | "build:demo": "npm run clean:demo && cross-env NODE_ENV=production TARGET=demo webpack --bail",
8 | "demo": "npm run clean:demo && cross-env NODE_ENV=development TARGET=demo webpack --bail",
9 | "clean": "rimraf dist style.css style.css.map",
10 | "clean:demo": "rimraf build",
11 | "start": "cross-env NODE_ENV=development TARGET=development webpack-dev-server --inline --hot",
12 | "lint": "eslint .",
13 | "prettier": "prettier --single-quote --trailing-comma es5 --write \"./**/*.{md,js,css}\" \"!./{build,dist}/**\" \"!./style.css*\"",
14 | "prepublishOnly": "npm run lint && npm run build",
15 | "test": "jest",
16 | "test:watch": "jest --watchAll",
17 | "deploy": "npm run build:demo && gh-pages -d build"
18 | },
19 | "main": "dist/main.js",
20 | "files": [
21 | "dist"
22 | ],
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/dpdenton/reactjs-file-uploader.git"
26 | },
27 | "authors": [
28 | "David Denton"
29 | ],
30 | "license": "MIT",
31 | "dependencies": {
32 | "prop-types": "^15.6.2"
33 | },
34 | "peerDependencies": {
35 | "react": "^15.5.0 || ^16.0.0",
36 | "react-dom": "^15.5.0 || ^16.0.0"
37 | },
38 | "devDependencies": {
39 | "@material-ui/core": "^1.3.0",
40 | "autoprefixer": "^8.6.4",
41 | "babel-cli": "^6.10.1",
42 | "babel-core": "^6.26.3",
43 | "babel-loader": "^7.1.4",
44 | "babel-plugin-transform-exponentiation-operator": "^6.8.0",
45 | "babel-plugin-transform-object-rest-spread": "^6.8.0",
46 | "babel-preset-env": "^1.7.0",
47 | "babel-preset-react": "^6.11.1",
48 | "cloudinary-react": "^1.0.6",
49 | "coveralls": "^3.0.2",
50 | "cross-env": "^5.2.0",
51 | "css-loader": "^0.28.11",
52 | "enzyme": "^3.3.0",
53 | "enzyme-adapter-react-16": "^1.1.1",
54 | "enzyme-to-json": "^3.3.4",
55 | "eslint": "^4.19.1",
56 | "eslint-config-airbnb": "^16.1.0",
57 | "eslint-config-prettier": "^2.9.0",
58 | "eslint-plugin-import": "^2.13.0",
59 | "eslint-plugin-jsx-a11y": "^6.0.3",
60 | "eslint-plugin-react": "^7.10.0",
61 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
62 | "file-loader": "^1.1.11",
63 | "gh-pages": "^1.2.0",
64 | "html-webpack-plugin": "^3.2.0",
65 | "jest": "^22.4.4",
66 | "jest-enzyme": "^6.0.2",
67 | "postcss-loader": "^2.1.5",
68 | "prettier": "^1.13.7",
69 | "react": "^16.4.1",
70 | "react-dom": "^16.4.1",
71 | "react-hot-loader": "^4.3.3",
72 | "rimraf": "^2.5.3",
73 | "style-loader": "^0.21.0",
74 | "uglifyjs-webpack-plugin": "^1.2.7",
75 | "webpack": "^4.14.0",
76 | "webpack-cli": "^2.1.5",
77 | "webpack-dev-server": "^3.1.4",
78 | "webpack-node-externals": "^1.7.2"
79 | },
80 | "keywords": [
81 | "react",
82 | "react-component",
83 | "file",
84 | "image",
85 | "upload",
86 | "uploader"
87 | ]
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/FileManager.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class FileManager extends React.Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | this._fileMap = {};
9 | this._addFileToMap = this._addFileToMap.bind(this);
10 | }
11 |
12 | componentWillMount() {
13 | this.props.files.forEach(this._addFileToMap);
14 | }
15 |
16 | componentWillReceiveProps(props) {
17 | props.files.forEach(this._addFileToMap);
18 | }
19 |
20 | render() {
21 | const keys = [];
22 | const files = this.props.files.filter(file => {
23 | const key = FileManager.generateKey(file.name + file.size + file.lastModified);
24 | Object.assign(file, {key});
25 | return keys.indexOf(key) === -1 ? keys.push(key) : false;
26 | });
27 | return this.props.children(files);
28 | }
29 |
30 | _addFileToMap(file) {
31 | const key = FileManager.generateKey(file.name + file.size + file.lastModified);
32 | this._fileMap[key] = file;
33 | }
34 |
35 | static generateKey(s) {
36 | let hash = 0;
37 | if (s.length === 0) return hash;
38 | for (let i = 0; i < s.length; i += 1) {
39 | const chr = s.charCodeAt(i);
40 | hash = ((hash << 5) - hash) + chr;
41 | hash |= 0; // Convert to 32bit integer
42 | }
43 | return hash;
44 | };
45 | }
46 |
47 | FileManager.propTypes = {
48 | files: PropTypes.array.isRequired,
49 | children: PropTypes.func.isRequired
50 | };
51 |
52 | export default FileManager;
--------------------------------------------------------------------------------
/src/components/FileUploader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class FileUploader extends React.Component {
5 |
6 | static get UPLOAD_READY() {
7 | return 'uploadReady';
8 | }
9 |
10 | static get UPLOAD_START() {
11 | return 'uploadStart';
12 | }
13 |
14 | static get UPLOAD_PROGRESS() {
15 | return 'uploadProgress';
16 | }
17 |
18 | static get UPLOAD_COMPLETE() {
19 | return 'uploadComplete';
20 | }
21 |
22 | static get DOWNLOAD_START() {
23 | return 'downloadStart';
24 | }
25 |
26 | static get DOWNLOAD_PROGRESS() {
27 | return 'downloadProgress';
28 | }
29 |
30 | static get DOWNLOAD_COMPLETE() {
31 | return 'downloadComplete';
32 | }
33 |
34 | static get ERROR() {
35 | return 'error';
36 | }
37 |
38 | static get ABORT() {
39 | return 'abort';
40 | }
41 |
42 | static get TIMEOUT() {
43 | return 'timeout';
44 | }
45 |
46 | constructor(props) {
47 | super(props);
48 | this.state = {
49 |
50 | // sequential request state
51 | uploadReady: null,
52 | uploadStart: null,
53 | uploadProgress: null,
54 | uploadComplete: null,
55 | downloadStart: null,
56 | downloadProgress: null,
57 | downloadComplete: null,
58 |
59 | // error state
60 | error: null,
61 | abort: null,
62 | timeout: null,
63 |
64 | // helper references
65 | requestState: null,
66 | request: null,
67 | response: null,
68 | readyState: null,
69 | fileData: null,
70 | file: props.file,
71 |
72 | // func references to start / abort request
73 | startUpload: null,
74 | abortRequest: null,
75 | };
76 | this._onEvent = this._onEvent.bind(this);
77 | this.onUploadStart = this.onUploadStart.bind(this);
78 | this.onUploadProgress = this.onUploadProgress.bind(this);
79 | this.onUploadComplete = this.onUploadComplete.bind(this);
80 | this.onDownloadStart = this.onDownloadStart.bind(this);
81 | this.onDownloadProgress = this.onDownloadProgress.bind(this);
82 | this.onDownloadComplete = this.onDownloadComplete.bind(this);
83 | this.onError = this.onError.bind(this);
84 | this.onAbort = this.onAbort.bind(this);
85 | this.onTimeout = this.onTimeout.bind(this);
86 | this.onReadyStateChange = this.onReadyStateChange.bind(this);
87 | this.startUpload = this.startUpload.bind(this);
88 | this.abortRequest = this.abortRequest.bind(this);
89 | }
90 |
91 | componentWillMount() {
92 | this._prepareRequest();
93 | }
94 |
95 | componentDidMount() {
96 | if (this.props.readFile) {
97 | const reader = new global.FileReader();
98 | reader.onload = e => this.onFileDataReady(e);
99 | reader.readAsDataURL(this.props.file);
100 | }
101 | this.onUploadReady(this.xhr);
102 | this.props.autoUpload && this.startUpload();
103 | }
104 |
105 | render() {
106 | return this.props.children(this.state)
107 | }
108 |
109 | componentWillReceiveProps(nextProps) {
110 | // make sure uploaded hasn't already started and that the upload prop has changed to true;
111 | if (this.state.uploadStart === null && nextProps.autoUpload && nextProps.autoUpload !== this.props.autoUpload) {
112 | this.startUpload();
113 | }
114 | }
115 |
116 | componentWillUnmount() {
117 | this.xhr.upload.removeEventListener("loadstart", this.onUploadStart);
118 | this.xhr.upload.removeEventListener("progress", this.onUploadProgress);
119 | this.xhr.upload.removeEventListener("loadend", this.onUploadComplete);
120 | this.xhr.removeEventListener("error", this.onError);
121 | this.xhr.removeEventListener("abort", this.onAbort);
122 | this.xhr.removeEventListener("timeout", this.onTimeout);
123 | this.xhr.onreadystatechange = null;
124 | }
125 |
126 | onFileDataReady(event) {
127 | // don't call _event because it isn't part of the sequential request state.
128 | this.setState({
129 | fileData: event.target.result,
130 | });
131 | this.props.onFileDataReady(event, this.state);
132 | }
133 |
134 | onUploadReady(event) {
135 | // provide ref to upload file if not immediately invoked.
136 | const newState = !this.props.autoUpload
137 | ? {file: this.props.file, startUpload: this.startUpload}
138 | : {file: this.props.file};
139 | this._onEvent(FileUploader.UPLOAD_READY, event, newState);
140 | }
141 |
142 | onUploadStart(event) {
143 | const newState = {
144 | request: this.xhr,
145 | startUpload: null,
146 | abortRequest: this.abortRequest,
147 | };
148 | this._onEvent(FileUploader.UPLOAD_START, event, newState);
149 | }
150 |
151 | onUploadProgress(event) {
152 | this._onEvent(FileUploader.UPLOAD_PROGRESS, event);
153 | };
154 |
155 | onUploadComplete(event) {
156 | this._onEvent(FileUploader.UPLOAD_COMPLETE, event);
157 | }
158 |
159 | onDownloadStart(event) {
160 | this._onEvent(FileUploader.DOWNLOAD_START, event);
161 | }
162 |
163 | onDownloadProgress(event) {
164 | const newState = {
165 | response: JSON.parse(event.currentTarget.responseText),
166 | };
167 | this._onEvent(FileUploader.DOWNLOAD_PROGRESS, event, newState);
168 | }
169 |
170 | onDownloadComplete(event) {
171 | const newState = {
172 | response: JSON.parse(event.currentTarget.responseText),
173 | abortRequest: null,
174 | };
175 | this._onEvent(FileUploader.DOWNLOAD_COMPLETE, event, newState);
176 | }
177 |
178 | onError(event) {
179 | this._onEvent(FileUploader.ERROR, event);
180 | }
181 |
182 | onAbort(event) {
183 | const newState = {
184 | abortRequest: null,
185 | };
186 | this._onEvent(FileUploader.ABORT, event, newState);
187 | }
188 |
189 | onTimeout(event) {
190 | this._onEvent(FileUploader.TIMEOUT, event);
191 | }
192 |
193 | onReadyStateChange(event) {
194 | this.props.onReadyStateChange(event);
195 | switch (event.currentTarget.readyState) {
196 | case 2:
197 | this.onDownloadStart(event);
198 | break;
199 | case 3:
200 | this.onDownloadProgress(event);
201 | break;
202 | case 4:
203 | this.onDownloadComplete(event);
204 | break;
205 | default:
206 | break;
207 | }
208 | }
209 |
210 | abortRequest() {
211 | this.xhr.abort();
212 | };
213 |
214 | startUpload() {
215 | const formData = new global.FormData();
216 | Object.keys(this.props.formData).forEach(key => formData.append(key, this.props.formData[key]));
217 | this.xhr.send(formData);
218 | };
219 |
220 | _prepareRequest() {
221 | // safe to do this before component has mounted as listeners aren't attached
222 | // when onreadystatechanges for 'UNSENT' and 'OPEN'.
223 | this.xhr = new global.XMLHttpRequest();
224 | this.xhr.open(this.props.method, this.props.url, true);
225 | Object.keys(this.props.headers).forEach(key => this.xhr.setRequestHeader(key, this.props.headers[key]));
226 |
227 | this.xhr.upload.addEventListener("loadstart", this.onUploadStart);
228 | this.xhr.upload.addEventListener("progress", this.onUploadProgress);
229 | this.xhr.upload.addEventListener("loadend", this.onUploadComplete);
230 | this.xhr.addEventListener("error", this.onError);
231 | this.xhr.addEventListener("abort", this.onAbort);
232 | this.xhr.addEventListener("timeout", this.onTimeout);
233 | this.xhr.onreadystatechange = this.onReadyStateChange;
234 | }
235 |
236 | _onEvent(eventName, event, newState = {}) {
237 | const eventState = {
238 | [eventName]: event,
239 | requestState: eventName,
240 | };
241 | this.props[FileUploader._onEventName(eventName)](event, this.state);
242 | this.setState(Object.assign(newState, eventState));
243 | }
244 |
245 | static _onEventName(eventName) {
246 | return `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`;
247 | }
248 | }
249 |
250 | FileUploader.propTypes = {
251 |
252 | // File to be uploaded
253 | file: PropTypes.instanceOf(global.File).isRequired,
254 |
255 | // url to POST to
256 | url: PropTypes.string.isRequired,
257 |
258 | // upload image immediately
259 | autoUpload: PropTypes.bool,
260 |
261 | // get file contents (to display preview image etc)
262 | readFile: PropTypes.bool,
263 |
264 | // request method
265 | method: PropTypes.string,
266 |
267 | // headers
268 | headers: PropTypes.object,
269 |
270 | // form data
271 | formData: PropTypes.object,
272 |
273 | // request upload events
274 | onUploadReady: PropTypes.func,
275 | onUploadStart: PropTypes.func,
276 | onUploadProgress: PropTypes.func,
277 | onUploadComplete: PropTypes.func,
278 | onDownloadStart: PropTypes.func,
279 | onDownloadProgress: PropTypes.func,
280 | onDownloadComplete: PropTypes.func,
281 | onReadyStateChange: PropTypes.func,
282 | onFileDataReady: PropTypes.func,
283 |
284 | // request events
285 | onError: PropTypes.func,
286 | onAbort: PropTypes.func,
287 | onTimeout: PropTypes.func,
288 |
289 | children: PropTypes.func.isRequired,
290 | };
291 |
292 | FileUploader.defaultProps = {
293 |
294 | autoUpload: false,
295 | readFile: false,
296 | method: 'POST',
297 | headers: {'X-Requested-With': 'XMLHttpRequest'},
298 | formData: {},
299 | onUploadReady: () => {
300 | },
301 | onUploadStart: () => {
302 | },
303 | onUploadProgress: () => {
304 | },
305 | onUploadComplete: () => {
306 | },
307 | onDownloadStart: () => {
308 | },
309 | onDownloadProgress: () => {
310 | },
311 | onDownloadComplete: () => {
312 | },
313 | onError: event => {
314 | global.alert(event.currentTarget.statusText || 'Something has gone wrong.');
315 | },
316 | onAbort: () => {
317 | },
318 | onTimeout: () => {
319 | },
320 | onReadyStateChange: () => {
321 | },
322 | onFileDataReady: () => {
323 | },
324 | };
325 |
326 |
327 | export default FileUploader;
328 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export {default as FileManager} from './components/FileManager';
2 | export {default as FileUploader} from './components/FileUploader';
3 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const autoprefixer = require('autoprefixer');
4 | const nodeExternals = require('webpack-node-externals');
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
7 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
8 |
9 | const target = process.env.TARGET || 'umd';
10 |
11 | const styleLoader = {
12 | loader: 'style-loader',
13 | options: {insertAt: 'top'},
14 | };
15 |
16 | const fileLoader = {
17 | loader: 'file-loader',
18 | options: {name: 'static/[name].[ext]'},
19 | };
20 |
21 | const postcssLoader = {
22 | loader: 'postcss-loader',
23 | options: {
24 | plugins: () => [autoprefixer()],
25 | },
26 | };
27 |
28 | const cssLoader = {
29 | loader: 'css-loader',
30 | options: {
31 | importLoaders: true,
32 | },
33 | };
34 |
35 | const defaultCssLoaders = [cssLoader, postcssLoader];
36 |
37 | const cssLoaders =
38 | target !== 'development' && target !== 'demo'
39 | ? ExtractTextPlugin.extract({
40 | fallback: styleLoader,
41 | use: defaultCssLoaders,
42 | })
43 | : [styleLoader, ...defaultCssLoaders];
44 |
45 | const config = {
46 | mode: 'production',
47 | entry: {'dist/main': './src/index'},
48 | output: {
49 | path: __dirname,
50 | filename: '[name].js',
51 | libraryTarget: 'umd',
52 | library: 'ReactImageLightbox',
53 | },
54 | devtool: 'source-map',
55 | plugins: [
56 | new webpack.EnvironmentPlugin({NODE_ENV: 'production'}),
57 | new webpack.optimize.OccurrenceOrderPlugin(),
58 | new ExtractTextPlugin('style.css'),
59 | ],
60 | module: {
61 | rules: [
62 | {
63 | test: /\.jsx?$/,
64 | use: ['babel-loader'],
65 | exclude: path.join(__dirname, 'node_modules'),
66 | },
67 | {
68 | test: /\.css$/,
69 | use: cssLoaders,
70 | exclude: [path.join(__dirname, 'examples')],
71 | },
72 | {
73 | test: /\.css$/,
74 | use: [styleLoader, ...defaultCssLoaders],
75 | include: path.join(__dirname, 'examples'),
76 | },
77 | ],
78 | },
79 | };
80 |
81 | switch (target) {
82 | case 'umd':
83 | // Exclude library dependencies from the bundle
84 | config.externals = [
85 | nodeExternals({
86 | // load non-javascript files with extensions, presumably via loaders
87 | whitelist: [/\.(?!(?:jsx?|json)$).{1,5}$/i],
88 | }),
89 | ];
90 |
91 | // Keep the minimizer from mangling variable names
92 | // (we keep minimization enabled to remove dead code)
93 | config.optimization = {
94 | minimizer: [
95 | new UglifyJSPlugin({
96 | uglifyOptions: {
97 | mangle: false,
98 | compress: {
99 | warnings: false,
100 | },
101 | output: {
102 | beautify: true,
103 | comments: true,
104 | },
105 | },
106 | }),
107 | ],
108 | };
109 | break;
110 | case 'development':
111 | config.mode = 'development';
112 | config.devtool = 'eval-source-map';
113 | config.module.rules.push({
114 | test: /\.(jpe?g|png|gif|ico|svg)$/,
115 | use: [fileLoader],
116 | exclude: path.join(__dirname, 'node_modules'),
117 | });
118 | config.entry = [
119 | 'react-hot-loader/patch',
120 | './examples/index'
121 | ];
122 | config.output = {
123 | path: path.join(__dirname, 'build'),
124 | filename: 'static/[name].js',
125 | };
126 | config.plugins = [
127 | new HtmlWebpackPlugin({
128 | inject: true,
129 | template: './examples/index.html',
130 | }),
131 | new webpack.EnvironmentPlugin({NODE_ENV: 'development'}),
132 | new webpack.NoEmitOnErrorsPlugin(),
133 | ];
134 | config.devServer = {
135 | contentBase: path.join(__dirname, 'build'),
136 | port: process.env.PORT || 3001,
137 | host: '0.0.0.0',
138 | disableHostCheck: true,
139 | stats: 'minimal',
140 | };
141 |
142 | break;
143 | case 'demo':
144 | config.module.rules.push({
145 | test: /\.(jpe?g|png|gif|ico|svg)$/,
146 | use: [fileLoader],
147 | exclude: path.join(__dirname, 'node_modules'),
148 | });
149 | config.entry = './examples/index';
150 | config.output = {
151 | path: path.join(__dirname, 'build'),
152 | filename: 'static/[name].js',
153 | };
154 | config.plugins = [
155 | new HtmlWebpackPlugin({
156 | inject: true,
157 | template: './examples/index.html',
158 | }),
159 | new webpack.EnvironmentPlugin({NODE_ENV: 'production'}),
160 | ];
161 |
162 | break;
163 | default:
164 | }
165 |
166 | module.exports = config;
167 |
--------------------------------------------------------------------------------