├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo ├── App.jsx ├── Demo.jsx ├── index.js ├── main.css └── normalize.css ├── dist-modules ├── index.js └── styles.js ├── dist ├── xhr-uploader.js ├── xhr-uploader.js.map ├── xhr-uploader.min.js └── xhr-uploader.min.js.map ├── lib ├── index_template.ejs └── render.jsx ├── package.json ├── src ├── index.js └── styles.js ├── tests ├── .eslintrc ├── setup.js └── xhr-upload.spec.js ├── webpack.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | gh-pages/ 2 | dist-modules/ 3 | dist/ 4 | build/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier", "prettier/react"], 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "env": { 11 | "es6": true, 12 | "browser": true, 13 | "mocha": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | build/ 36 | gh-pages/ 37 | dist-modules/ 38 | node_modules/ 39 | 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | lib/ 3 | tests/ 4 | src/ 5 | gh-pages/ 6 | dist/ 7 | .* 8 | karma.conf.js 9 | webpack.config.babel.js 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | cache: 5 | directories: 6 | - node_modules 7 | branches: 8 | only: 9 | - master 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, RMA Consulting 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of react-xhr-uploader nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React XHR Uploader 2 | 3 | [![npm version](https://badge.fury.io/js/react-xhr-uploader.svg)](https://badge.fury.io/js/react-xhr-uploader) 4 | [![Build Status](https://travis-ci.org/rma-consulting/react-xhr-uploader.svg?branch=master)](https://travis-ci.org/rma-consulting/react-xhr-uploader) 5 | 6 | React component for uploading files with XMLHTTPRequest v2 7 | 8 | Check full documentation with examples at [https://harunhasdal.github.io/react-xhr-uploader](https://harunhasdal.github.io/react-xhr-uploader) 9 | 10 | Pull requests are welcome. 11 | 12 | ### How to run/develop locally 13 | 14 | Use `npm start` to run the webpack development server at localhost:8080. Hot module replacement is enabled. 15 | 16 | Use `npm test` to run the tests. Use `npm test:tdd` to run the tests continuously in watch mode. 17 | 18 | Use `npm run test:lint` to run ESLint checks. 19 | 20 | ### Example express server 21 | 22 | You will need a server that would accept post requests for multipart file uploads. Below is a sample express server to test out the examples. 23 | 24 | ```js 25 | const express = require('express'); 26 | const Busboy = require('busboy'); 27 | const fs = require('fs'); 28 | 29 | const port = 3000; 30 | const app = express(); 31 | 32 | app.all('/api/uploadfile', function(req, res, next) { 33 | res.header("Access-Control-Allow-Origin", "*"); 34 | res.header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type"); 35 | next(); 36 | }); 37 | 38 | app.post('/api/uploadfile', (req, res) => { 39 | const busboy = new Busboy({ headers: req.headers }); 40 | busboy.on('file', function(fieldname, file, filename) { 41 | let saveTo = __dirname + '/uploads/' + fieldname + '-' + filename + Date.now(); 42 | file.pipe(fs.createWriteStream(saveTo)); 43 | }); 44 | busboy.on('finish', function() { 45 | res.end('done'); 46 | }); 47 | res.on('close', function() { 48 | req.unpipe(busboy); 49 | }); 50 | req.pipe(busboy); 51 | }); 52 | 53 | app.listen(port, '0.0.0.0', function () { 54 | console.log(`Uploader server listening on port ${port}!`); 55 | }); 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /demo/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Demo from './Demo.jsx'; 3 | 4 | const App = () => ( 5 |
6 |

Demonstrating the XHR Uploader component

7 | 8 |
9 | ); 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /demo/Demo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import XHRUploader from '../src/index.js'; 3 | 4 | const UPLOAD_URL = 'http://localhost:3000/api/uploadfile'; 5 | 6 | const Demo = () => ( 7 |
8 |
9 |

Default usage comes up with an upload button for manual instantiation of the upload and supports one file.

10 |
 11 |         {`
 12 |         
 15 |       `}
 16 |       
17 | 18 |
19 |
20 |

By default, the component uses POST method for file transfer. The component accepts the

21 |
method
22 |

property to specify different http method.

23 |
 24 |         {`
 25 |           
 29 |         `}
 30 |       
31 | 32 |
33 |
34 |

You can enable automatic upload after drag and drop or file selection with

35 |
auto
36 |

property to get rid of the upload button.

37 |
 38 |         {`
 39 |           
 43 |         `}
 44 |       
45 | 46 |
47 |
48 |

You can enable multiple file support with

49 |
maxFiles
50 |

property

51 |
 52 |         {`
 53 |           
 58 |         `}
 59 |       
60 | 61 |
62 |
63 |

You can enable chunked file upload with

64 |
chunks
and
chunkSize

properties

65 |
 66 |         {`
 67 |       
 74 |     `}
 75 |       
76 | 77 |
78 |
79 |

Pass more form data with

80 |
formData
81 |
 82 |         {`
 83 |         
 97 |       `}
 98 |       
99 | 100 |
101 |
102 |

Customising look and feel

103 |

The component accepts a

104 |
style
105 |

property as a javascript object to override the default styles of the component.

106 |

Following is the default styles object the component uses. You can modify this object with your styles and pass in props.

107 |
108 |         {`
109 |           const defaultStyles = {
110 |             root: {
111 |               border: '1px solid #CACACA',
112 |               padding: 20
113 |             },
114 |             dropTargetStyle: {
115 |               border: '3px dashed #4A90E2',
116 |               padding: 10,
117 |               backgroundColor: '#ffffff',
118 |               cursor: 'pointer'
119 |             },
120 |             dropTargetActiveStyle: {
121 |               backgroundColor: '#ccffcc'
122 |             },
123 |             placeHolderStyle: {
124 |               paddingLeft: '20%',
125 |               paddingRight: '20%',
126 |               textAlign: 'center'
127 |             },
128 |             uploadButtonStyle: {
129 |               width: '100%',
130 |               marginTop: 10,
131 |               height: 32,
132 |               alignSelf: 'center',
133 |               cursor: 'pointer',
134 |               backgroundColor: '#D9EBFF',
135 |               border: '1px solid #5094E3',
136 |               fontSize: 14
137 |             },
138 |             fileset: {
139 |               marginTop: 10,
140 |               paddingTop: 10,
141 |               paddingBottom: 10,
142 |               borderTop: '1px solid #CACACA'
143 |             },
144 |             fileDetails: {
145 |               paddingTop: 10,
146 |               display: 'flex',
147 |               alignItems: 'flex-start'
148 |             },
149 |             fileName: {
150 |               flexGrow: '8'
151 |             },
152 |             fileSize: {
153 |               'float': 'right',
154 |               flexGrow: '2',
155 |               alignSelf: 'flex-end'
156 |             },
157 |             removeButton: {
158 |               alignSelf: 'flex-end'
159 |             },
160 |             progress: {
161 |               marginTop: 10,
162 |               width: '100%',
163 |               height: 16,
164 |               WebkitAppearance: 'none'
165 |             }
166 |           };
167 |         `}
168 |       
169 |

170 | You do not need to pass in the entire style object in order to modify a particular inline style... E.g. if you need to override only 171 | the 172 |

173 |
root
174 |

style, then you can pass your styles object as follows:

175 |
{`
176 |         const myStyles = {root: {border: 'none', padding: 'none'}};
177 |         
181 |       `}
182 |

XhrUploader will set the default styles above for all properties except for the ones that you override.

183 |
184 |

Styling upload action controls

185 |

For upload action controls, supply the following props as class names:

186 |
cancelIconClass
187 |
completeIconClass
188 |
uploadIconClass
189 |

If the above class names are not provided, default Font Awesome classes, e.g. 'fa fa-close', are stored.

190 |

You will need to install FontAwesome if you would like to use their icons.

191 |
192 |

Styling progress bar

193 |

194 | The upload progress bar is styled as HTML5 native progress bar of the host browser. To customise it, supply the following class 195 | name: 196 |

197 |
progressClass
198 |
199 |
200 | ); 201 | 202 | export default Demo; 203 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App.jsx'; 4 | 5 | const app = document.getElementsByClassName('demonstration')[0]; 6 | 7 | ReactDOM.render(, app); 8 | -------------------------------------------------------------------------------- /demo/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 62.5%; 3 | } 4 | body { 5 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 6 | font-size: 1.4em; 7 | line-height: 1.42857143; 8 | color: #333; 9 | background-color: #fff; 10 | margin: 0; 11 | } 12 | 13 | ul { 14 | list-style: none; 15 | padding: 0; 16 | } 17 | 18 | pre { 19 | font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; 20 | margin-bottom: 10px; 21 | overflow: auto; 22 | width: auto; 23 | padding: 0px 10px; 24 | background-color: #eee; 25 | } 26 | 27 | h2 { 28 | font-size: 40px; 29 | font-weight: 400; 30 | margin-bottom: 5px; 31 | } 32 | 33 | .container { 34 | width: 1170px; 35 | padding-right: 15px; 36 | padding-left: 15px; 37 | margin-right: auto; 38 | margin-left: auto; 39 | } 40 | 41 | .introduction { 42 | text-align: center; 43 | } 44 | 45 | article { 46 | display: block !important; 47 | margin-top: 6em; 48 | margin-left: auto; 49 | margin-right: auto; 50 | max-width: 768px; 51 | } 52 | 53 | article section { 54 | margin-bottom: 2em; 55 | } 56 | 57 | article h2 { 58 | margin-top: 2em; 59 | } 60 | 61 | pre { 62 | background: #fafefe; 63 | padding: 0.5em; 64 | } 65 | 66 | article .description { 67 | margin: 1em; 68 | padding: 1em; 69 | max-width: 60em; 70 | background: #fafafa; 71 | border: 0.1em solid #eee; 72 | } 73 | 74 | article .description h2 { 75 | margin: 0; 76 | } 77 | 78 | /* hompage */ 79 | img { 80 | max-width: 100%; 81 | } 82 | 83 | a { 84 | color: #2ea3f2; 85 | } 86 | 87 | .header { 88 | color: white; 89 | width: 100%; 90 | min-height: 10em; 91 | text-align: center; 92 | overflow: hidden; 93 | background-image:linear-gradient(#2ea3f2, #003142); 94 | } 95 | 96 | .github-fork-ribbon-wrapper { 97 | width: 15em; 98 | height: 15em; 99 | position: absolute; 100 | overflow: hidden; 101 | top: 0px; 102 | right: 0px; 103 | z-index: 9999; 104 | pointer-events: none; 105 | } 106 | 107 | .github-fork-ribbon { 108 | position: absolute; 109 | padding: 2px 0px; 110 | background-color: rgb(0, 153, 0); 111 | background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.14902)); 112 | box-shadow: rgba(0, 0, 0, 0.498039) 0px 2px 3px 0px; 113 | font-style: normal; 114 | font-variant: normal; 115 | font-weight: 700; 116 | font-stretch: normal; 117 | font-size: 1.3rem; 118 | line-height: normal; 119 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 120 | z-index: 9999; 121 | pointer-events: auto; 122 | top: 4.2rem; 123 | right: -4.3rem; 124 | transform: rotate(45deg); 125 | } 126 | 127 | .github-fork-ribbon a { 128 | color: rgb(255, 255, 255); 129 | text-decoration: none; 130 | text-shadow: rgba(0, 0, 0, 0.498039) 0px -1px; 131 | text-align: center; 132 | width: 200px; 133 | line-height: 20px; 134 | display: inline-block; 135 | padding: 2px 0px; 136 | border-width: 1px 0px; 137 | border-style: dotted; 138 | border-color: rgba(255, 255, 255, 0.701961); 139 | } 140 | .grid { 141 | background-color: transparent; 142 | background-image: linear-gradient(0deg, transparent 24%, 143 | rgba(255, 255, 255, .1) 25%, 144 | rgba(255, 255, 255, .1) 26%, 145 | transparent 27%, 146 | transparent 74%, 147 | rgba(255, 255, 255, .1) 75%, 148 | rgba(255, 255, 255, .1) 76%, 149 | transparent 77%, transparent), 150 | linear-gradient(90deg, transparent 24%, 151 | rgba(255, 255, 255, .1) 25%, 152 | rgba(255, 255, 255, .1) 26%, 153 | transparent 27%, 154 | transparent 74%, rgba(255, 255, 255, .1) 75%, 155 | rgba(255, 255, 255, .1) 76%, 156 | transparent 77%, transparent); 157 | height:100%; 158 | width: 100%; 159 | background-size:50px 50px; 160 | padding: 1em; 161 | } 162 | 163 | .header h1 { 164 | text-shadow: 1px 1px 1px rgba(0,0,0,0.5); 165 | } 166 | 167 | @media (min-width: 45em) { 168 | .header h1 { 169 | font-size: 3em; 170 | } 171 | } 172 | 173 | .header p { 174 | margin: 0 auto 2em; 175 | max-width: 25em; 176 | } 177 | 178 | .header-nav { 179 | background-color: #003142; 180 | } 181 | 182 | .header-nav ul { 183 | list-style: none; 184 | margin: 0; 185 | padding: 0; 186 | } 187 | 188 | .header-nav ul li { 189 | display: inline-block; 190 | } 191 | 192 | .header-nav ul li a { 193 | color: #fba600; 194 | display: block; 195 | padding: 1em; 196 | text-decoration: none; 197 | } 198 | 199 | .container { 200 | max-width: 60em; 201 | } 202 | 203 | .row { 204 | display: block; 205 | background-color: #f9f9f9; 206 | border: 1px solid #e0e0e0; 207 | width: 100%; 208 | margin-top: -1px; 209 | } 210 | 211 | .row:before, 212 | .row:after { 213 | content: " "; 214 | display: table; 215 | } 216 | .row:after { 217 | clear: both; 218 | } 219 | 220 | .m50 { 221 | display: block; 222 | float: none; 223 | width: 100%; 224 | } 225 | 226 | .m50 h2 { 227 | border-bottom: 1px solid #2ea3f2; 228 | margin-top: 0; 229 | } 230 | 231 | .m50 h2 a { 232 | color: #003142; 233 | text-decoration: none; 234 | } 235 | 236 | .m50 h2 a:hover { 237 | color: #2ea3f2; 238 | } 239 | 240 | @media (min-width: 45em) { 241 | .container { 242 | margin: 2em auto; 243 | } 244 | .row { 245 | margin-top: 0; 246 | margin-bottom: 2em; 247 | } 248 | .m50 { 249 | width: 50%; 250 | float: left; 251 | } 252 | } 253 | 254 | .img { 255 | background-color: white; 256 | padding: 1em 1em; 257 | } 258 | 259 | .copy { 260 | padding: 1em; 261 | } 262 | 263 | .menu__all-charts { 264 | color: #fba600 !important; 265 | } 266 | -------------------------------------------------------------------------------- /demo/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type='button'], 292 | /* 1 */ input[type='reset'], 293 | input[type='submit'] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type='checkbox'], 335 | input[type='radio'] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type='number']::-webkit-inner-spin-button, 347 | input[type='number']::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type='search'] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type='search']::-webkit-search-cancel-button, 371 | input[type='search']::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /dist-modules/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _propTypes = require('prop-types'); 14 | 15 | var _propTypes2 = _interopRequireDefault(_propTypes); 16 | 17 | var _reactTransitionGroup = require('react-transition-group'); 18 | 19 | var _styles = require('./styles'); 20 | 21 | var _styles2 = _interopRequireDefault(_styles); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 26 | 27 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 28 | 29 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 30 | 31 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 32 | 33 | var XHRUploader = function (_Component) { 34 | _inherits(XHRUploader, _Component); 35 | 36 | function XHRUploader(props) { 37 | _classCallCheck(this, XHRUploader); 38 | 39 | var _this = _possibleConstructorReturn(this, (XHRUploader.__proto__ || Object.getPrototypeOf(XHRUploader)).call(this, props)); 40 | 41 | _this.state = { items: [], styles: Object.assign({}, _styles2.default, props.styles) }; 42 | _this.activeDrag = 0; 43 | _this.xhrs = []; 44 | _this.onClick = _this.onClick.bind(_this); 45 | _this.onUploadButtonClick = _this.onUploadButtonClick.bind(_this); 46 | _this.onFileSelect = _this.onFileSelect.bind(_this); 47 | _this.onDragEnter = _this.onDragEnter.bind(_this); 48 | _this.onDragLeave = _this.onDragLeave.bind(_this); 49 | _this.onDrop = _this.onDrop.bind(_this); 50 | return _this; 51 | } 52 | 53 | _createClass(XHRUploader, [{ 54 | key: 'onClick', 55 | value: function onClick() { 56 | this.fileInput.click(); 57 | } 58 | }, { 59 | key: 'onUploadButtonClick', 60 | value: function onUploadButtonClick() { 61 | this.upload(); 62 | } 63 | }, { 64 | key: 'onFileSelect', 65 | value: function onFileSelect() { 66 | var _this2 = this; 67 | 68 | var items = this.filesToItems(this.fileInput.files); 69 | this.setState({ items: items }, function () { 70 | if (_this2.props.auto) { 71 | _this2.upload(); 72 | } 73 | }); 74 | } 75 | }, { 76 | key: 'onDragEnter', 77 | value: function onDragEnter() { 78 | this.activeDrag += 1; 79 | this.setState({ isActive: this.activeDrag > 0 }); 80 | } 81 | }, { 82 | key: 'onDragOver', 83 | value: function onDragOver(e) { 84 | if (e) { 85 | e.preventDefault(); 86 | } 87 | return false; 88 | } 89 | }, { 90 | key: 'onDragLeave', 91 | value: function onDragLeave() { 92 | this.activeDrag -= 1; 93 | if (this.activeDrag === 0) { 94 | this.setState({ isActive: false }); 95 | } 96 | } 97 | }, { 98 | key: 'onDrop', 99 | value: function onDrop(e) { 100 | var _this3 = this; 101 | 102 | if (!e) { 103 | return; 104 | } 105 | e.preventDefault(); 106 | this.activeDrag = 0; 107 | var droppedFiles = e.dataTransfer ? e.dataTransfer.files : []; 108 | var items = this.filesToItems(droppedFiles); 109 | 110 | this.setState({ isActive: false, items: items }, function () { 111 | if (_this3.props.auto) { 112 | _this3.upload(); 113 | } 114 | }); 115 | } 116 | }, { 117 | key: 'clearIfAllCompleted', 118 | value: function clearIfAllCompleted() { 119 | var _this4 = this; 120 | 121 | if (this.props.clearTimeOut > 0) { 122 | var completed = this.state.items.filter(function (item) { 123 | return item.progress === 100; 124 | }).length; 125 | if (completed === this.state.items.length) { 126 | setTimeout(function () { 127 | _this4.setState({ items: [] }); 128 | }, this.props.clearTimeOut); 129 | } 130 | } 131 | } 132 | }, { 133 | key: 'updateFileProgress', 134 | value: function updateFileProgress(index, progress) { 135 | var newItems = [].concat(_toConsumableArray(this.state.items)); 136 | newItems[index] = Object.assign({}, this.state.items[index], { progress: progress }); 137 | this.setState({ items: newItems }, this.clearIfAllCompleted); 138 | } 139 | }, { 140 | key: 'updateFileChunkProgress', 141 | value: function updateFileChunkProgress(index, chunkIndex, progress) { 142 | var newItems = [].concat(_toConsumableArray(this.state.items)); 143 | var currentItem = this.state.items[index]; 144 | var newProgressArr = [].concat(_toConsumableArray(currentItem.chunkProgress)); 145 | var totalProgress = newProgressArr.reduce(function (a, b) { 146 | return a + b; 147 | }) / (newProgressArr.length - 1); 148 | // -1 because there is always single chunk for "0" percentage pushed as chunkProgress.push(0) in method filesToItems) 149 | newProgressArr[chunkIndex] = progress; 150 | newItems[index] = Object.assign({}, currentItem, { chunkProgress: newProgressArr, progress: totalProgress }); 151 | this.setState({ items: newItems }, this.clearIfAllCompleted); 152 | } 153 | }, { 154 | key: 'cancelFile', 155 | value: function cancelFile(index) { 156 | var newItems = [].concat(_toConsumableArray(this.state.items)); 157 | newItems[index] = Object.assign({}, this.state.items[index], { cancelled: true }); 158 | if (this.xhrs[index]) { 159 | this.xhrs[index].upload.removeEventListener('progress'); 160 | this.xhrs[index].removeEventListener('load'); 161 | this.xhrs[index].abort(); 162 | } 163 | this.setState({ items: newItems }); 164 | } 165 | }, { 166 | key: 'upload', 167 | value: function upload() { 168 | var _this5 = this; 169 | 170 | var items = this.state.items; 171 | if (items) { 172 | items.filter(function (item) { 173 | return !item.cancelled; 174 | }).forEach(function (item) { 175 | _this5.uploadItem(item); 176 | }); 177 | } 178 | } 179 | }, { 180 | key: 'uploadItem', 181 | value: function uploadItem(item) { 182 | var _this6 = this; 183 | 184 | if (this.props.chunks && item.file) { 185 | var BYTES_PER_CHUNK = this.props.chunkSize; 186 | var SIZE = item.file.size; 187 | 188 | var start = 0; 189 | var end = BYTES_PER_CHUNK; 190 | 191 | var chunkProgressHandler = function chunkProgressHandler(percentage, chunkIndex) { 192 | _this6.updateFileChunkProgress(item.index, chunkIndex, percentage); 193 | }; 194 | var chunkIndex = 0; 195 | while (start < SIZE) { 196 | this.uploadChunk(item.file.slice(start, end), chunkIndex += 1, item.file.name, chunkProgressHandler); 197 | start = end; 198 | end = start + BYTES_PER_CHUNK; 199 | } 200 | } else { 201 | this.uploadFile(item.file, function (progress) { 202 | return _this6.updateFileProgress(item.index, progress); 203 | }); 204 | } 205 | } 206 | }, { 207 | key: 'uploadChunk', 208 | value: function uploadChunk(blob, chunkIndex, fileName, progressCallback) { 209 | if (blob) { 210 | var formData = new FormData(); 211 | var xhr = new XMLHttpRequest(); 212 | var data = this.props.formData; 213 | 214 | if (data.length > 0) { 215 | data.map(function (d) { 216 | formData.append(d.name, d.value); 217 | }); 218 | } 219 | 220 | formData.append(this.props.fieldName, blob, fileName + '-chunk' + chunkIndex); 221 | 222 | xhr.onload = function () { 223 | progressCallback(100, chunkIndex); 224 | }; 225 | xhr.upload.onprogress = function (e) { 226 | if (e.lengthComputable) { 227 | progressCallback(e.loaded / e.total * 100, chunkIndex); 228 | } 229 | }; 230 | xhr.open(this.props.method, this.props.url, true); 231 | xhr.send(formData); 232 | } 233 | } 234 | }, { 235 | key: 'uploadFile', 236 | value: function uploadFile(file, progressCallback) { 237 | if (file) { 238 | var formData = new FormData(); 239 | var xhr = new XMLHttpRequest(); 240 | var data = this.props.formData; 241 | 242 | if (data.length > 0) { 243 | data.map(function (d) { 244 | formData.append(d.name, d.value); 245 | }); 246 | } 247 | 248 | formData.append(this.props.fieldName, file, file.name); 249 | 250 | xhr.onload = function () { 251 | progressCallback(100); 252 | }; 253 | 254 | xhr.upload.onprogress = function (e) { 255 | if (e.lengthComputable) { 256 | progressCallback(e.loaded / e.total * 100); 257 | } 258 | }; 259 | 260 | xhr.open(this.props.method, this.props.url, true); 261 | xhr.send(formData); 262 | this.xhrs[file.index] = xhr; 263 | } 264 | } 265 | }, { 266 | key: 'filesToItems', 267 | value: function filesToItems(files) { 268 | var _this7 = this; 269 | 270 | var fileItems = Array.prototype.slice.call(files).slice(0, this.props.maxFiles); 271 | var items = fileItems.map(function (f, i) { 272 | if (_this7.props.chunks) { 273 | var chunkProgress = []; 274 | for (var j = 0; j <= f.size / _this7.props.chunkSize; j += 1) { 275 | chunkProgress.push(0); 276 | } 277 | return { file: f, index: i, progress: 0, cancelled: false, chunkProgress: chunkProgress }; 278 | } 279 | return { file: f, index: i, progress: 0, cancelled: false }; 280 | }); 281 | return items; 282 | } 283 | }, { 284 | key: 'humanFileSize', 285 | value: function humanFileSize(bytes, si) { 286 | var thresh = si ? 1000 : 1024; 287 | if (Math.abs(bytes) < thresh) { 288 | return bytes + " B"; 289 | } 290 | var units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; 291 | var u = -1; 292 | do { 293 | bytes /= thresh; 294 | ++u; 295 | } while (Math.abs(bytes) >= thresh && u < units.length - 1); 296 | return bytes.toFixed(1) + " " + units[u]; 297 | } 298 | }, { 299 | key: 'renderDropTarget', 300 | value: function renderDropTarget() { 301 | var uploadIconClass = this.props.uploadIconClass; 302 | var styles = this.state.styles; 303 | 304 | var dropTargetStyle = styles.dropTargetStyle; 305 | if (this.state.isActive) { 306 | dropTargetStyle = Object.assign({}, dropTargetStyle, styles.dropTargetActiveStyle); 307 | } 308 | return _react2.default.createElement( 309 | 'div', 310 | { 311 | 'data-test-id': 'dropTarget', 312 | style: dropTargetStyle, 313 | onClick: this.onClick, 314 | onDragEnter: this.onDragEnter, 315 | onDragOver: this.onDragOver, 316 | onDragLeave: this.onDragLeave, 317 | onDrop: this.onDrop 318 | }, 319 | _react2.default.createElement( 320 | 'div', 321 | { style: styles.placeHolderStyle }, 322 | _react2.default.createElement( 323 | 'p', 324 | null, 325 | this.props.dropzoneLabel 326 | ), 327 | _react2.default.createElement('i', { className: uploadIconClass }) 328 | ), 329 | this.renderFileSet() 330 | ); 331 | } 332 | }, { 333 | key: 'renderFileSet', 334 | value: function renderFileSet() { 335 | var _this8 = this; 336 | 337 | var items = this.state.items; 338 | var _props = this.props, 339 | progressClass = _props.progressClass, 340 | transitionName = _props.filesetTransitionName; 341 | 342 | if (items.length > 0) { 343 | var _props2 = this.props, 344 | cancelIconClass = _props2.cancelIconClass, 345 | completeIconClass = _props2.completeIconClass; 346 | var _state = this.state, 347 | progress = _state.progress, 348 | styles = _state.styles; 349 | 350 | var cancelledItems = items.filter(function (item) { 351 | return item.cancelled === true; 352 | }); 353 | var filesetStyle = items.length === cancelledItems.length ? { display: 'none' } : styles.fileset; 354 | return _react2.default.createElement( 355 | _reactTransitionGroup.TransitionGroup, 356 | { 357 | component: 'div', 358 | transitionName: transitionName, 359 | transitionEnterTimeout: 0, 360 | transitionLeaveTimeout: 0 361 | }, 362 | _react2.default.createElement( 363 | 'div', 364 | { style: filesetStyle }, 365 | items.filter(function (item) { 366 | return !item.cancelled && !!item.file; 367 | }).map(function (item) { 368 | var file = item.file; 369 | var iconClass = item.progress < 100 ? cancelIconClass : completeIconClass; 370 | return _react2.default.createElement( 371 | 'div', 372 | { key: item.index }, 373 | _react2.default.createElement( 374 | 'div', 375 | { style: styles.fileDetails }, 376 | _react2.default.createElement( 377 | 'span', 378 | { className: 'icon-file icon-large' }, 379 | '\xA0' 380 | ), 381 | _react2.default.createElement( 382 | 'span', 383 | { style: styles.fileName }, 384 | file.name + ', ' + file.type 385 | ), 386 | _react2.default.createElement( 387 | 'span', 388 | { style: styles.fileSize }, 389 | '' + _this8.humanFileSize(file.size) 390 | ), 391 | _react2.default.createElement('i', { 392 | className: iconClass, 393 | style: { cursor: 'pointer' }, 394 | onClick: function onClick(e) { 395 | e.stopPropagation(); 396 | _this8.cancelFile(item.index); 397 | } 398 | }) 399 | ), 400 | _react2.default.createElement( 401 | 'div', 402 | null, 403 | _react2.default.createElement( 404 | 'progress', 405 | { 406 | style: progressClass ? {} : styles.progress, 407 | className: progressClass, 408 | min: '0', 409 | max: '100', 410 | value: item.progress 411 | }, 412 | item.progress, 413 | '%' 414 | ) 415 | ) 416 | ); 417 | }) 418 | ) 419 | ); 420 | } 421 | return _react2.default.createElement(_reactTransitionGroup.TransitionGroup, { 422 | component: 'div', 423 | transitionName: transitionName, 424 | transitionEnterTimeout: 0, 425 | transitionLeaveTimeout: 0 426 | }); 427 | } 428 | }, { 429 | key: 'renderButton', 430 | value: function renderButton() { 431 | var styles = this.state.styles; 432 | 433 | var displayButton = !this.props.auto; 434 | if (displayButton) { 435 | return _react2.default.createElement( 436 | 'button', 437 | { style: styles.uploadButtonStyle, onClick: this.onUploadButtonClick }, 438 | this.props.buttonLabel 439 | ); 440 | } 441 | return null; 442 | } 443 | }, { 444 | key: 'renderInput', 445 | value: function renderInput() { 446 | var _this9 = this; 447 | 448 | var maxFiles = this.props.maxFiles; 449 | return _react2.default.createElement('input', { 450 | style: { display: 'none' }, 451 | multiple: maxFiles > 1, 452 | type: 'file', 453 | ref: function ref(c) { 454 | if (c) { 455 | _this9.fileInput = c; 456 | } 457 | }, 458 | onChange: this.onFileSelect 459 | }); 460 | } 461 | }, { 462 | key: 'render', 463 | value: function render() { 464 | var styles = this.state.styles; 465 | 466 | return _react2.default.createElement( 467 | 'div', 468 | { style: styles.root }, 469 | this.renderDropTarget(), 470 | this.renderButton(), 471 | this.renderInput() 472 | ); 473 | } 474 | }]); 475 | 476 | return XHRUploader; 477 | }(_react.Component); 478 | 479 | XHRUploader.propTypes = { 480 | url: _propTypes2.default.string.isRequired, 481 | method: _propTypes2.default.string, 482 | auto: _propTypes2.default.bool, 483 | fieldName: _propTypes2.default.string, 484 | buttonLabel: _propTypes2.default.string, 485 | dropzoneLabel: _propTypes2.default.string, 486 | chunks: _propTypes2.default.bool, 487 | chunkSize: _propTypes2.default.number, 488 | maxFiles: _propTypes2.default.number, 489 | clearTimeOut: _propTypes2.default.number, 490 | filesetTransitionName: _propTypes2.default.string, 491 | styles: _propTypes2.default.shape({}), 492 | cancelIconClass: _propTypes2.default.string, 493 | completeIconClass: _propTypes2.default.string, 494 | uploadIconClass: _propTypes2.default.string, 495 | progressClass: _propTypes2.default.string, 496 | formData: _propTypes2.default.arrayOf(_propTypes2.default.shape({ 497 | name: _propTypes2.default.string, 498 | value: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.number]) 499 | })) 500 | }; 501 | 502 | XHRUploader.defaultProps = { 503 | method: 'POST', 504 | auto: false, 505 | fieldName: 'datafile', 506 | buttonLabel: 'Upload', 507 | dropzoneLabel: 'Drag and drop your files here or pick them from your computer', 508 | maxSize: 25 * 1024 * 1024, 509 | chunks: false, 510 | chunkSize: 512 * 1024, 511 | localStore: false, 512 | maxFiles: 1, 513 | encrypt: false, 514 | clearTimeOut: 3000, 515 | filesetTransitionName: 'fileset', 516 | cancelIconClass: 'fa fa-close', 517 | completeIconClass: 'fa fa-check', 518 | uploadIconClass: 'fa fa-upload', 519 | formData: [] 520 | }; 521 | 522 | exports.default = XHRUploader; -------------------------------------------------------------------------------- /dist-modules/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var styles = { 7 | root: { 8 | border: '1px solid #CACACA', 9 | padding: 20 10 | }, 11 | dropTargetStyle: { 12 | border: '3px dashed #f2e745', 13 | padding: 10, 14 | backgroundColor: '#fefcea', 15 | cursor: 'pointer' 16 | }, 17 | dropTargetActiveStyle: { 18 | backgroundColor: '#ccffcc' 19 | }, 20 | placeHolderStyle: { 21 | paddingLeft: '20%', 22 | paddingRight: '20%', 23 | textAlign: 'center' 24 | }, 25 | uploadButtonStyle: { 26 | width: '100%', 27 | marginTop: 10, 28 | height: 32, 29 | alignSelf: 'center', 30 | cursor: 'pointer', 31 | backgroundColor: '#fefcea', 32 | border: '1px solid #f2e745', 33 | fontSize: 14 34 | }, 35 | fileset: { 36 | marginTop: 10, 37 | paddingTop: 10, 38 | paddingBottom: 10 39 | }, 40 | fileDetails: { 41 | paddingTop: 10, 42 | display: 'flex', 43 | alignItems: 'flex-start' 44 | }, 45 | fileName: { 46 | flexGrow: '8' 47 | }, 48 | fileSize: { 49 | float: 'right', 50 | flexGrow: '2', 51 | alignSelf: 'flex-end' 52 | }, 53 | removeButton: { 54 | alignSelf: 'flex-end' 55 | }, 56 | progress: { 57 | WebkitAppearance: 'none', 58 | appearance: 'none', 59 | marginTop: 10, 60 | width: '100%', 61 | height: 16 62 | } 63 | }; 64 | 65 | exports.default = styles; -------------------------------------------------------------------------------- /lib/index_template.ejs: -------------------------------------------------------------------------------- 1 | 2 | manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title || 'Webpack App'%> 6 | 7 | <% if (htmlWebpackPlugin.files.favicon) { %> 8 | 9 | <% } %> 10 | 11 | <% if (htmlWebpackPlugin.options.mobile) { %> 12 | 13 | <% } %> 14 | 15 | <% for (var css in htmlWebpackPlugin.files.css) { %> 16 | 17 | <% } %> 18 | 19 | <% if (htmlWebpackPlugin.options.baseHref) { %> 20 | 21 | <% } %> 22 | 23 | 24 | 25 |
26 |
27 | <% if (htmlWebpackPlugin.options.name) { %> 28 |

<%= htmlWebpackPlugin.options.name %>

29 | <% } %> 30 | 31 | <% if (htmlWebpackPlugin.options.description) { %> 32 |
<%= htmlWebpackPlugin.options.description %>
33 | <% } %> 34 |
35 |
36 | 41 |
42 |
43 |
44 |
45 |

Demonstration

46 |
47 | <%= htmlWebpackPlugin.options.demonstration %> 48 |
49 | 50 |

Documentation

51 |
52 | <%= htmlWebpackPlugin.options.documentation %> 53 |
54 |
55 |
56 | 57 | <% for (var chunk in htmlWebpackPlugin.files.chunks) { %> 58 | 59 | <% } %> 60 | 61 | 62 | -------------------------------------------------------------------------------- /lib/render.jsx: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom/server'; 6 | import remark from 'remark'; 7 | import reactRenderer from 'remark-react'; 8 | 9 | export default function (rootPath, context, template) { 10 | const demoTemplate = template || ''; 11 | const readme = fs.readFileSync(path.join(rootPath, 'README.md'), 'utf8'); 12 | const renderedMarkup = remark().use(reactRenderer).process(readme); 13 | return { 14 | name: context.name, 15 | description: context.description, 16 | demonstration: demoTemplate, 17 | documentation: ReactDOM.renderToStaticMarkup( 18 |
19 | {renderedMarkup} 20 |
21 | ) 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-xhr-uploader", 3 | "description": "ReactJS component for XHR (level 2) file upload", 4 | "author": "Harun Hasdal", 5 | "user": "harunhasdal", 6 | "version": "0.5.0", 7 | "scripts": { 8 | "start": "cross-env NODE_ENV=development webpack-dev-server --config ./webpack.config.babel.js --inline --hot", 9 | "test:lint": "eslint . --ext .js --ext .jsx", 10 | "test": "mocha --require tests/setup.js --compilers js:node_modules/babel-core/register tests/**/*.spec.js", 11 | "test:watch": "npm test -- -w", 12 | "gh-pages": "webpack", 13 | "gh-pages:deploy": "gh-pages -d gh-pages", 14 | "gh-pages:stats": "webpack --profile --json > stats.json", 15 | "dist": "webpack", 16 | "dist:min": "webpack", 17 | "dist:modules": "babel ./src --out-dir ./dist-modules", 18 | "pretest": "npm run test:lint", 19 | "preversion": "npm run test && npm run dist && npm run dist:min && git commit --allow-empty -am \"Update dist\"", 20 | "prepublish": "npm run dist:modules", 21 | "postpublish": "npm run gh-pages && npm run gh-pages:deploy" 22 | }, 23 | "main": "dist-modules/index.js", 24 | "peerDependencies": { 25 | "react": "^15.0.0 || ^16.0.0", 26 | "react-dom": "^15.0.0 || ^16.0.0" 27 | }, 28 | "devDependencies": { 29 | "babel-cli": "^6.4.5", 30 | "babel-core": "^6.4.5", 31 | "babel-eslint": "^6.1.2", 32 | "babel-loader": "^6.2.1", 33 | "babel-preset-airbnb": "^2.0.0", 34 | "babel-preset-es2015": "^6.3.13", 35 | "babel-preset-react": "^6.3.13", 36 | "babel-preset-stage-0": "^6.5.0", 37 | "babel-register": "^6.4.3", 38 | "chai": "^3.5.0", 39 | "clean-webpack-plugin": "^0.1.7", 40 | "cross-env": "^2.0.1", 41 | "css-loader": "^0.25.0", 42 | "enzyme": "^3.0.0", 43 | "enzyme-adapter-react-16": "^1.0.0", 44 | "eslint": "^4.18.2", 45 | "eslint-config-prettier": "2.6.0", 46 | "eslint-loader": "^1.2.1", 47 | "eslint-plugin-import": "^1.15.0", 48 | "eslint-plugin-jsx-a11y": "^2.2.2", 49 | "eslint-plugin-react": "^6.2.1", 50 | "extract-text-webpack-plugin": "^1.0.1", 51 | "file-loader": "^0.9.0", 52 | "gh-pages": "^0.11.0", 53 | "git-prepush-hook": "^1.0.1", 54 | "highlight.js": "^10.4.1", 55 | "html-webpack-plugin": "^2.7.2", 56 | "isparta-instrumenter-loader": "^1.0.0", 57 | "jsdom": "9.11.0", 58 | "json-loader": "^0.5.4", 59 | "mocha": "^3.0.2", 60 | "prop-types": "^15.6.0", 61 | "purecss": "^0.6.0", 62 | "react": "^16.0.0", 63 | "react-addons-css-transition-group": "^15.6.2", 64 | "react-dom": "^16.0.0", 65 | "react-hot-loader": "^3.0.0-beta.3", 66 | "react-test-renderer": "^16.0.0", 67 | "remark": "^3.2.2", 68 | "remark-react": "^1.1.0", 69 | "style-loader": "^0.13.0", 70 | "sync-exec": "^0.6.2", 71 | "system-bell-webpack-plugin": "^1.0.0", 72 | "url-loader": "^0.5.7", 73 | "webpack": "^1.13.2", 74 | "webpack-dev-server": ">=3.1.11", 75 | "webpack-merge": "^0.14.1", 76 | "react-transition-group": "^2.2.0" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "https://github.com/rma-consulting/react-xhr-uploader" 81 | }, 82 | "homepage": "https://rma-consulting.github.io/react-xhr-uploader/", 83 | "bugs": { 84 | "url": "https://github.com/rma-consulting/react-xhr-uploader/issues" 85 | }, 86 | "keywords": [ 87 | "react", 88 | "reactjs", 89 | "xhr", 90 | "file-upload", 91 | "XMLHTTPRequest", 92 | "blob-upload", 93 | "large-file-upload", 94 | "react-component" 95 | ], 96 | "license": "LICENCE", 97 | "pre-push": [], 98 | "dependencies": {} 99 | } 100 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { TransitionGroup } from 'react-transition-group'; 4 | import defaultStyles from './styles'; 5 | 6 | class XHRUploader extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { items: [], styles: Object.assign({}, defaultStyles, props.styles) }; 10 | this.activeDrag = 0; 11 | this.xhrs = []; 12 | this.onClick = this.onClick.bind(this); 13 | this.onUploadButtonClick = this.onUploadButtonClick.bind(this); 14 | this.onFileSelect = this.onFileSelect.bind(this); 15 | this.onDragEnter = this.onDragEnter.bind(this); 16 | this.onDragLeave = this.onDragLeave.bind(this); 17 | this.onDrop = this.onDrop.bind(this); 18 | } 19 | 20 | onClick() { 21 | this.fileInput.click(); 22 | } 23 | 24 | onUploadButtonClick() { 25 | this.upload(); 26 | } 27 | 28 | onFileSelect() { 29 | const items = this.filesToItems(this.fileInput.files); 30 | this.setState({ items }, () => { 31 | if (this.props.auto) { 32 | this.upload(); 33 | } 34 | }); 35 | } 36 | 37 | onDragEnter() { 38 | this.activeDrag += 1; 39 | this.setState({ isActive: this.activeDrag > 0 }); 40 | } 41 | 42 | onDragOver(e) { 43 | if (e) { 44 | e.preventDefault(); 45 | } 46 | return false; 47 | } 48 | 49 | onDragLeave() { 50 | this.activeDrag -= 1; 51 | if (this.activeDrag === 0) { 52 | this.setState({ isActive: false }); 53 | } 54 | } 55 | 56 | onDrop(e) { 57 | if (!e) { 58 | return; 59 | } 60 | e.preventDefault(); 61 | this.activeDrag = 0; 62 | const droppedFiles = e.dataTransfer ? e.dataTransfer.files : []; 63 | const items = this.filesToItems(droppedFiles); 64 | 65 | this.setState({ isActive: false, items }, () => { 66 | if (this.props.auto) { 67 | this.upload(); 68 | } 69 | }); 70 | } 71 | 72 | clearIfAllCompleted() { 73 | if (this.props.clearTimeOut > 0) { 74 | const completed = this.state.items.filter(item => item.progress === 100).length; 75 | if (completed === this.state.items.length) { 76 | setTimeout(() => { 77 | this.setState({ items: [] }); 78 | }, this.props.clearTimeOut); 79 | } 80 | } 81 | } 82 | 83 | updateFileProgress(index, progress) { 84 | const newItems = [...this.state.items]; 85 | newItems[index] = Object.assign({}, this.state.items[index], { progress }); 86 | this.setState({ items: newItems }, this.clearIfAllCompleted); 87 | } 88 | 89 | updateFileChunkProgress(index, chunkIndex, progress) { 90 | const newItems = [...this.state.items]; 91 | const currentItem = this.state.items[index]; 92 | const newProgressArr = [...currentItem.chunkProgress]; 93 | const totalProgress = newProgressArr.reduce((a, b) => a + b) / (newProgressArr.length - 1); 94 | // -1 because there is always single chunk for "0" percentage pushed as chunkProgress.push(0) in method filesToItems) 95 | newProgressArr[chunkIndex] = progress; 96 | newItems[index] = Object.assign({}, currentItem, { chunkProgress: newProgressArr, progress: totalProgress }); 97 | this.setState({ items: newItems }, this.clearIfAllCompleted); 98 | } 99 | 100 | cancelFile(index) { 101 | const newItems = [...this.state.items]; 102 | newItems[index] = Object.assign({}, this.state.items[index], { cancelled: true }); 103 | if (this.xhrs[index]) { 104 | this.xhrs[index].upload.removeEventListener('progress'); 105 | this.xhrs[index].removeEventListener('load'); 106 | this.xhrs[index].abort(); 107 | } 108 | this.setState({ items: newItems }); 109 | } 110 | 111 | upload() { 112 | const items = this.state.items; 113 | if (items) { 114 | items.filter(item => !item.cancelled).forEach(item => { 115 | this.uploadItem(item); 116 | }); 117 | } 118 | } 119 | 120 | uploadItem(item) { 121 | if (this.props.chunks && item.file) { 122 | const BYTES_PER_CHUNK = this.props.chunkSize; 123 | const SIZE = item.file.size; 124 | 125 | let start = 0; 126 | let end = BYTES_PER_CHUNK; 127 | 128 | const chunkProgressHandler = (percentage, chunkIndex) => { 129 | this.updateFileChunkProgress(item.index, chunkIndex, percentage); 130 | }; 131 | let chunkIndex = 0; 132 | while (start < SIZE) { 133 | this.uploadChunk(item.file.slice(start, end), (chunkIndex += 1), item.file.name, chunkProgressHandler); 134 | start = end; 135 | end = start + BYTES_PER_CHUNK; 136 | } 137 | } else { 138 | this.uploadFile(item.file, progress => this.updateFileProgress(item.index, progress)); 139 | } 140 | } 141 | 142 | uploadChunk(blob, chunkIndex, fileName, progressCallback) { 143 | if (blob) { 144 | const formData = new FormData(); 145 | const xhr = new XMLHttpRequest(); 146 | const data = this.props.formData; 147 | 148 | if(data.length > 0){ 149 | data.map( d => { 150 | formData.append(d.name, d.value); 151 | }) 152 | } 153 | 154 | formData.append(this.props.fieldName, blob, `${fileName}-chunk${chunkIndex}`); 155 | 156 | xhr.onload = () => { 157 | progressCallback(100, chunkIndex); 158 | }; 159 | xhr.upload.onprogress = e => { 160 | if (e.lengthComputable) { 161 | progressCallback(e.loaded / e.total * 100, chunkIndex); 162 | } 163 | }; 164 | xhr.open(this.props.method, this.props.url, true); 165 | xhr.send(formData); 166 | } 167 | } 168 | 169 | uploadFile(file, progressCallback) { 170 | if (file) { 171 | const formData = new FormData(); 172 | const xhr = new XMLHttpRequest(); 173 | const data = this.props.formData; 174 | 175 | if(data.length > 0){ 176 | data.map( d => { 177 | formData.append(d.name, d.value); 178 | }) 179 | } 180 | 181 | formData.append(this.props.fieldName, file, file.name); 182 | 183 | xhr.onload = () => { 184 | progressCallback(100); 185 | }; 186 | 187 | xhr.upload.onprogress = e => { 188 | if (e.lengthComputable) { 189 | progressCallback(e.loaded / e.total * 100); 190 | } 191 | }; 192 | 193 | xhr.open(this.props.method, this.props.url, true); 194 | xhr.send(formData); 195 | this.xhrs[file.index] = xhr; 196 | } 197 | } 198 | 199 | filesToItems(files) { 200 | const fileItems = Array.prototype.slice.call(files).slice(0, this.props.maxFiles); 201 | const items = fileItems.map((f, i) => { 202 | if (this.props.chunks) { 203 | const chunkProgress = []; 204 | for (let j = 0; j <= f.size / this.props.chunkSize; j += 1) { 205 | chunkProgress.push(0); 206 | } 207 | return { file: f, index: i, progress: 0, cancelled: false, chunkProgress }; 208 | } 209 | return { file: f, index: i, progress: 0, cancelled: false }; 210 | }); 211 | return items; 212 | } 213 | 214 | humanFileSize(bytes, si) { 215 | var thresh = si ? 1000 : 1024; 216 | if(Math.abs(bytes) < thresh) { 217 | return bytes + " B"; 218 | } 219 | var units = si 220 | ? ['kB','MB','GB','TB','PB','EB','ZB','YB'] 221 | : ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']; 222 | var u = -1; 223 | do { 224 | bytes /= thresh; 225 | ++u; 226 | } while(Math.abs(bytes) >= thresh && u < units.length - 1); 227 | return bytes.toFixed(1) + " " + units[u]; 228 | } 229 | 230 | renderDropTarget() { 231 | const { uploadIconClass } = this.props; 232 | const { styles } = this.state; 233 | let dropTargetStyle = styles.dropTargetStyle; 234 | if (this.state.isActive) { 235 | dropTargetStyle = Object.assign({}, dropTargetStyle, styles.dropTargetActiveStyle); 236 | } 237 | return ( 238 |
247 |
248 |

{this.props.dropzoneLabel}

249 | 250 |
251 | {this.renderFileSet()} 252 |
253 | ); 254 | } 255 | 256 | renderFileSet() { 257 | const items = this.state.items; 258 | const { progressClass, filesetTransitionName: transitionName } = this.props; 259 | if (items.length > 0) { 260 | const { cancelIconClass, completeIconClass } = this.props; 261 | const { progress, styles } = this.state; 262 | const cancelledItems = items.filter(item => item.cancelled === true); 263 | const filesetStyle = items.length === cancelledItems.length ? { display: 'none' } : styles.fileset; 264 | return ( 265 | 271 |
272 | {items.filter(item => !item.cancelled && !!item.file).map(item => { 273 | const file = item.file; 274 | const iconClass = item.progress < 100 ? cancelIconClass : completeIconClass; 275 | return ( 276 |
277 |
278 |   279 | {`${file.name}, ${file.type}`} 280 | {`${this.humanFileSize(file.size)}`} 281 | { 285 | e.stopPropagation(); 286 | this.cancelFile(item.index); 287 | }} 288 | /> 289 |
290 |
291 | 298 | {item.progress}% 299 | 300 |
301 |
302 | ); 303 | })} 304 |
305 |
306 | ); 307 | } 308 | return ( 309 | 315 | ); 316 | } 317 | 318 | renderButton() { 319 | const { styles } = this.state; 320 | const displayButton = !this.props.auto; 321 | if (displayButton) { 322 | return ( 323 | 326 | ); 327 | } 328 | return null; 329 | } 330 | 331 | renderInput() { 332 | const maxFiles = this.props.maxFiles; 333 | return ( 334 | 1} 337 | type="file" 338 | ref={c => { 339 | if (c) { 340 | this.fileInput = c; 341 | } 342 | }} 343 | onChange={this.onFileSelect} 344 | /> 345 | ); 346 | } 347 | 348 | render() { 349 | const { styles } = this.state; 350 | return ( 351 |
352 | {this.renderDropTarget()} 353 | {this.renderButton()} 354 | {this.renderInput()} 355 |
356 | ); 357 | } 358 | } 359 | 360 | XHRUploader.propTypes = { 361 | url: PropTypes.string.isRequired, 362 | method: PropTypes.string, 363 | auto: PropTypes.bool, 364 | fieldName: PropTypes.string, 365 | buttonLabel: PropTypes.string, 366 | dropzoneLabel: PropTypes.string, 367 | chunks: PropTypes.bool, 368 | chunkSize: PropTypes.number, 369 | maxFiles: PropTypes.number, 370 | clearTimeOut: PropTypes.number, 371 | filesetTransitionName: PropTypes.string, 372 | styles: PropTypes.shape({}), 373 | cancelIconClass: PropTypes.string, 374 | completeIconClass: PropTypes.string, 375 | uploadIconClass: PropTypes.string, 376 | progressClass: PropTypes.string, 377 | formData: PropTypes.arrayOf(PropTypes.shape({ 378 | name: PropTypes.string, 379 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 380 | })) 381 | }; 382 | 383 | XHRUploader.defaultProps = { 384 | method: 'POST', 385 | auto: false, 386 | fieldName: 'datafile', 387 | buttonLabel: 'Upload', 388 | dropzoneLabel: 'Drag and drop your files here or pick them from your computer', 389 | maxSize: 25 * 1024 * 1024, 390 | chunks: false, 391 | chunkSize: 512 * 1024, 392 | localStore: false, 393 | maxFiles: 1, 394 | encrypt: false, 395 | clearTimeOut: 3000, 396 | filesetTransitionName: 'fileset', 397 | cancelIconClass: 'fa fa-close', 398 | completeIconClass: 'fa fa-check', 399 | uploadIconClass: 'fa fa-upload', 400 | formData: [] 401 | }; 402 | 403 | export default XHRUploader; 404 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | const styles = { 2 | root: { 3 | border: '1px solid #CACACA', 4 | padding: 20 5 | }, 6 | dropTargetStyle: { 7 | border: '3px dashed #f2e745', 8 | padding: 10, 9 | backgroundColor: '#fefcea', 10 | cursor: 'pointer' 11 | }, 12 | dropTargetActiveStyle: { 13 | backgroundColor: '#ccffcc' 14 | }, 15 | placeHolderStyle: { 16 | paddingLeft: '20%', 17 | paddingRight: '20%', 18 | textAlign: 'center' 19 | }, 20 | uploadButtonStyle: { 21 | width: '100%', 22 | marginTop: 10, 23 | height: 32, 24 | alignSelf: 'center', 25 | cursor: 'pointer', 26 | backgroundColor: '#fefcea', 27 | border: '1px solid #f2e745', 28 | fontSize: 14 29 | }, 30 | fileset: { 31 | marginTop: 10, 32 | paddingTop: 10, 33 | paddingBottom: 10 34 | }, 35 | fileDetails: { 36 | paddingTop: 10, 37 | display: 'flex', 38 | alignItems: 'flex-start' 39 | }, 40 | fileName: { 41 | flexGrow: '8' 42 | }, 43 | fileSize: { 44 | float: 'right', 45 | flexGrow: '2', 46 | alignSelf: 'flex-end' 47 | }, 48 | removeButton: { 49 | alignSelf: 'flex-end' 50 | }, 51 | progress: { 52 | WebkitAppearance: 'none', 53 | appearance: 'none', 54 | marginTop: 10, 55 | width: '100%', 56 | height: 16 57 | } 58 | }; 59 | 60 | export default styles; 61 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../.eslintrc" 4 | ], 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "rules": { 11 | "no-console": 0, 12 | "no-unused-expressions": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | const jsdom = require('jsdom').jsdom; 3 | 4 | // Set up dummy DOM and provide `window` and `document.` 5 | global.document = jsdom(''); 6 | global.window = document.defaultView; 7 | const exposedProperties = ['window', 'navigator', 'document']; 8 | Object.keys(document.defaultView).forEach(property => { 9 | if (typeof global[property] === 'undefined') { 10 | exposedProperties.push(property); 11 | global[property] = document.defaultView[property]; 12 | } 13 | }); 14 | global.requestAnimationFrame = function(callback) { 15 | setTimeout(callback, 0); 16 | }; 17 | // Set variables and cookies here 18 | global.navigator = { 19 | userAgent: 'node.js', 20 | plugins: [] 21 | }; 22 | -------------------------------------------------------------------------------- /tests/xhr-upload.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import XHRUploader from '../src/index.js'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import { expect } from 'chai'; 6 | 7 | Enzyme.configure({ adapter: new Adapter() }); 8 | 9 | describe('XHRUploader component', () => { 10 | it('should be defined', () => { 11 | expect(XHRUploader).to.exist; 12 | }); 13 | let wrapper; 14 | beforeEach(() => { 15 | wrapper = shallow(); 16 | }); 17 | 18 | describe('when rendered into the document', () => { 19 | it('should render', () => { 20 | const instance = wrapper.instance(); 21 | expect(instance).to.be.instanceOf(XHRUploader); 22 | }); 23 | 24 | it('should have no items in state initially', () => { 25 | expect(wrapper.state().items.length).to.equal(0); 26 | }); 27 | 28 | it('should have active state when there is an active drag event', () => { 29 | const dropTarget = wrapper.find('[data-test-id="dropTarget"]'); 30 | dropTarget.simulate('dragEnter'); 31 | expect(wrapper.state().isActive).to.equal(true); 32 | dropTarget.simulate('dragOver'); 33 | expect(wrapper.state().isActive).to.equal(true); 34 | dropTarget.simulate('dragLeave'); 35 | expect(wrapper.state().isActive).to.equal(false); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | import * as path from 'path'; 3 | 4 | import webpack from 'webpack'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 7 | import SystemBellPlugin from 'system-bell-webpack-plugin'; 8 | import Clean from 'clean-webpack-plugin'; 9 | import merge from 'webpack-merge'; 10 | 11 | import renderJSX from './lib/render.jsx'; 12 | import pkg from './package.json'; 13 | 14 | const TARGET = process.env.npm_lifecycle_event; 15 | const ROOT_PATH = __dirname; 16 | const config = { 17 | paths: { 18 | dist: path.join(ROOT_PATH, 'dist'), 19 | src: path.join(ROOT_PATH, 'src'), 20 | demo: path.join(ROOT_PATH, 'demo'), 21 | tests: path.join(ROOT_PATH, 'tests') 22 | }, 23 | filename: 'xhr-uploader', 24 | library: 'XHRUploader' 25 | }; 26 | const CSS_PATHS = [ 27 | config.paths.demo, 28 | path.join(ROOT_PATH, 'node_modules/purecss'), 29 | path.join(ROOT_PATH, 'node_modules/highlight.js/styles/github.css'), 30 | path.join(ROOT_PATH, 'node_modules/react-ghfork/gh-fork-ribbon.ie.css'), 31 | path.join(ROOT_PATH, 'node_modules/react-ghfork/gh-fork-ribbon.css') 32 | ]; 33 | const STYLE_ENTRIES = [ 34 | 'purecss', 35 | 'highlight.js/styles/github.css', 36 | './demo/normalize.css', 37 | './demo/main.css' 38 | ]; 39 | 40 | process.env.BABEL_ENV = TARGET; 41 | 42 | const demoCommon = { 43 | resolve: { 44 | extensions: ['', '.js', '.jsx', '.css', '.png', '.jpg'] 45 | }, 46 | module: { 47 | preLoaders: [ 48 | { 49 | test: /\.jsx?$/, 50 | loaders: ['eslint'], 51 | include: [ 52 | config.paths.demo, 53 | config.paths.src 54 | ] 55 | } 56 | ], 57 | loaders: [ 58 | { 59 | test: /\.png$/, 60 | loader: 'url?limit=100000&mimetype=image/png', 61 | include: config.paths.demo 62 | }, 63 | { 64 | test: /\.jpg$/, 65 | loader: 'file', 66 | include: config.paths.demo 67 | }, 68 | { 69 | test: /\.json$/, 70 | loader: 'json', 71 | include: path.join(ROOT_PATH, 'package.json') 72 | } 73 | ] 74 | }, 75 | plugins: [ 76 | new SystemBellPlugin() 77 | ] 78 | }; 79 | 80 | if (TARGET === 'start') { 81 | module.exports = merge(demoCommon, { 82 | devtool: 'eval-source-map', 83 | entry: { 84 | demo: [config.paths.demo].concat(STYLE_ENTRIES) 85 | }, 86 | plugins: [ 87 | new webpack.DefinePlugin({ 88 | 'process.env.NODE_ENV': '"development"' 89 | }), 90 | new HtmlWebpackPlugin(Object.assign({}, { 91 | title: `${pkg.name} - ${pkg.description}`, 92 | githubUser: `${pkg.user}`, 93 | githubProjectName: `${pkg.name}`, 94 | template: 'lib/index_template.ejs', 95 | inject: false 96 | }, renderJSX(__dirname, pkg))) 97 | ], 98 | module: { 99 | loaders: [ 100 | { 101 | test: /\.css$/, 102 | loaders: ['style', 'css'], 103 | include: CSS_PATHS 104 | }, 105 | { 106 | test: /\.jsx?$/, 107 | loaders: ['babel?cacheDirectory'], 108 | include: [ 109 | config.paths.demo, 110 | config.paths.src 111 | ] 112 | } 113 | ] 114 | }, 115 | devServer: { 116 | historyApiFallback: true, 117 | progress: true, 118 | host: process.env.HOST, 119 | port: process.env.PORT, 120 | stats: 'info' 121 | } 122 | }); 123 | } 124 | 125 | function NamedModulesPlugin(options) { 126 | this.options = options || {}; 127 | } 128 | NamedModulesPlugin.prototype.apply = (compiler) => { 129 | compiler.plugin('compilation', (compilation) => { 130 | compilation.plugin('before-module-ids', (modules) => { 131 | modules.forEach((module) => { 132 | if (module.id === null && module.libIdent) { 133 | const id = module.libIdent({ 134 | context: compiler.options.context 135 | }); 136 | 137 | // Skip CSS files since those go through ExtractTextPlugin 138 | if (!id.endsWith('.css')) { 139 | Object.assign(module, {id}); 140 | } 141 | } 142 | }); 143 | }); 144 | }); 145 | }; 146 | 147 | if (TARGET === 'gh-pages' || TARGET === 'gh-pages:stats') { 148 | module.exports = merge(demoCommon, { 149 | entry: { 150 | app: config.paths.demo, 151 | vendors: [ 152 | 'react' 153 | ], 154 | style: STYLE_ENTRIES 155 | }, 156 | output: { 157 | path: './gh-pages', 158 | filename: '[name].[chunkhash].js', 159 | chunkFilename: '[chunkhash].js' 160 | }, 161 | plugins: [ 162 | new Clean(['gh-pages'], { 163 | verbose: false 164 | }), 165 | new ExtractTextPlugin('[name].[chunkhash].css'), 166 | new webpack.DefinePlugin({ 167 | // This affects the react lib size 168 | 'process.env.NODE_ENV': '"production"' 169 | }), 170 | new HtmlWebpackPlugin(Object.assign({}, { 171 | title: `${pkg.name} - ${pkg.description}`, 172 | githubUser: `${pkg.user}`, 173 | githubProjectName: `${pkg.name}`, 174 | template: 'lib/index_template.ejs', 175 | inject: false 176 | }, renderJSX(__dirname, pkg))), 177 | new NamedModulesPlugin(), 178 | new webpack.optimize.DedupePlugin(), 179 | new webpack.optimize.UglifyJsPlugin({ 180 | compress: { 181 | warnings: false 182 | } 183 | }), 184 | new webpack.optimize.CommonsChunkPlugin({ 185 | names: ['vendors', 'manifest'] 186 | }) 187 | ], 188 | module: { 189 | loaders: [ 190 | { 191 | test: /\.css$/, 192 | loader: ExtractTextPlugin.extract('style', 'css'), 193 | include: CSS_PATHS 194 | }, 195 | { 196 | test: /\.jsx?$/, 197 | loaders: ['babel'], 198 | include: [ 199 | config.paths.demo, 200 | config.paths.src 201 | ] 202 | } 203 | ] 204 | } 205 | }); 206 | } 207 | 208 | // !TARGET === prepush hook for test 209 | if (TARGET === 'test' || TARGET === 'test:tdd' || !TARGET) { 210 | module.exports = merge(demoCommon, { 211 | module: { 212 | preLoaders: [ 213 | { 214 | test: /\.jsx?$/, 215 | loaders: ['eslint'], 216 | include: [ 217 | config.paths.tests 218 | ] 219 | } 220 | ], 221 | loaders: [ 222 | { 223 | test: /\.jsx?$/, 224 | loaders: ['babel?cacheDirectory'], 225 | include: [ 226 | config.paths.src, 227 | config.paths.tests 228 | ] 229 | } 230 | ] 231 | } 232 | }); 233 | } 234 | 235 | const distCommon = { 236 | devtool: 'source-map', 237 | output: { 238 | path: config.paths.dist, 239 | libraryTarget: 'umd', 240 | library: config.library 241 | }, 242 | entry: config.paths.src, 243 | externals: { 244 | 'react': { 245 | commonjs: 'react', 246 | commonjs2: 'react', 247 | amd: 'React', 248 | root: 'React' 249 | } 250 | }, 251 | module: { 252 | loaders: [ 253 | { 254 | test: /\.jsx?$/, 255 | loaders: ['babel'], 256 | include: config.paths.src 257 | } 258 | ] 259 | }, 260 | plugins: [ 261 | new SystemBellPlugin() 262 | ] 263 | }; 264 | 265 | if (TARGET === 'dist') { 266 | module.exports = merge(distCommon, { 267 | output: { 268 | filename: `${config.filename}.js` 269 | } 270 | }); 271 | } 272 | 273 | if (TARGET === 'dist:min') { 274 | module.exports = merge(distCommon, { 275 | output: { 276 | filename: `${config.filename}.min.js` 277 | }, 278 | plugins: [ 279 | new webpack.optimize.UglifyJsPlugin({ 280 | compress: { 281 | warnings: false 282 | } 283 | }) 284 | ] 285 | }); 286 | } 287 | --------------------------------------------------------------------------------