Skip to content

Commit 630af11

Browse files
author
Jonatan E. Salas
authored
Merge pull request #27 from redux-autoform/experimental
FileUpload Component
2 parents a1f89bc + 98f8533 commit 630af11

File tree

13 files changed

+604
-4
lines changed

13 files changed

+604
-4
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/demo/uploads
12
/node_modules
23
/lib
34
/.idea

demo/Server.js

+29
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,29 @@ import webpackMiddleware from 'webpack-dev-middleware';
88
import webpackHotMiddleware from 'webpack-hot-middleware';
99
import webpack from 'webpack';
1010

11+
import multer from 'multer';
12+
13+
const storage = multer.diskStorage({
14+
destination: function(request, file, callback) {
15+
callback(null, "./demo/uploads");
16+
},
17+
filename: function(request, file, callback) {
18+
callback(null, file.originalname + '-' + Date.now())
19+
}
20+
});
21+
22+
const upload = multer({storage});
23+
1124
const webpackCompiler = webpack(webpackConfig);
1225

1326
require.extensions['.html'] = function (module, filename) {
1427
module.exports = fs.readFileSync(filename, 'utf8');
1528
};
1629

1730
const development = process.env.NODE_ENV !== 'production';
31+
1832
let app = express();
33+
1934
let router = new Router();
2035

2136
router.get("/api/languages", (request, response) => {
@@ -37,6 +52,20 @@ router.get("/api/languages", (request, response) => {
3752
response.status(200).json(arr);
3853
});
3954

55+
router.post("/upload", (request, response) => {
56+
57+
upload.array("fileData")(request, response, (err) => {
58+
if(err) {
59+
response.json({status: false, message: "There was an error while uploading files."});
60+
return;
61+
}
62+
63+
response.json({status: true, message: "Files correctly uploaded."});
64+
65+
})
66+
67+
});
68+
4069
app.use(router);
4170

4271
if (development) {

demo/less/styles.less

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@import '../../node_modules/bootstrap/less/bootstrap.less';
22
@import '../../node_modules/font-awesome/less/font-awesome.less';
33
@import '../../node_modules/react-widgets/dist/css/react-widgets.css';
4-
@import '../../node_modules/react-select/dist/react-select.css';
4+
@import '../../node_modules/react-select-plus/dist/react-select-plus.css';
55
@import '../../src/styles/styles';
66
@import '../../src/styles/styles-defaultfactories';
77
@import './LiveSchemaEditor.less';

demo/presets.js

+10
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,15 @@ export default [
124124
layoutName: 'edit',
125125
formTitle: 'Edit contact',
126126
schema: require('./presets/componentsFieldGroup.txt')
127+
},
128+
{
129+
name: 'componentsFileUpload',
130+
displayName: 'Components - FileUpload (Experimental)',
131+
entityName: 'contact',
132+
layoutName: 'edit',
133+
formTitle: 'Edit contact',
134+
schema: require('./presets/componentsFileUpload.txt')
127135
}
136+
137+
128138
]

demo/presets/componentsFileUpload.txt

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
entities: [
3+
{
4+
name: "contact",
5+
fields: [
6+
{
7+
name: "fileUploader",
8+
type: "string",
9+
10+
}
11+
],
12+
layouts: [
13+
{
14+
name: "edit",
15+
fields: [
16+
{
17+
name: "fileUploader",
18+
displayName: "File Upload",
19+
component: "FileUpload",
20+
url: "/upload"
21+
22+
}
23+
]
24+
}
25+
]
26+
}
27+
]
28+
}

demo/uploads/.placeholder

Whitespace-only changes.

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@
2828
},
2929
"homepage": "https://github.com/redux-autoform/redux-autoform-bootstrap-ui#readme",
3030
"dependencies": {
31+
"attr-accept": "^1.0.3",
32+
"body-parser": "^1.15.2",
3133
"bootstrap": "^3.3.6",
34+
"filesize": "^3.3.0",
3235
"font-awesome": "^4.6.3",
3336
"isomorphic-fetch": "^2.2.1",
37+
"multer": "^1.2.0",
3438
"react": "^15.1.0",
3539
"react-bootstrap": "^0.30.0",
3640
"react-dom": "^15.2.1",

src/components/common/DropZone.js

+266
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import accepts from 'attr-accept';
2+
import React, { Component, PropTypes } from 'react';
3+
4+
export default class DropZone extends Component {
5+
static propTypes = {
6+
// Overriding drop behavior
7+
onDrop: PropTypes.func,
8+
onDropAccepted: PropTypes.func,
9+
onDropRejected: PropTypes.func,
10+
11+
// Overriding drag behavior
12+
onDragStart: PropTypes.func,
13+
onDragEnter: PropTypes.func,
14+
onDragLeave: PropTypes.func,
15+
16+
children: PropTypes.node, // Contents of the dropzone
17+
style: PropTypes.object, // CSS styles to apply
18+
activeStyle: PropTypes.object, // CSS styles to apply when drop will be accepted
19+
rejectStyle: PropTypes.object, // CSS styles to apply when drop will be rejected
20+
className: PropTypes.string, // Optional className
21+
activeClassName: PropTypes.string, // className for accepted state
22+
rejectClassName: PropTypes.string, // className for rejected state
23+
24+
disablePreview: PropTypes.bool, // Enable/disable preview generation
25+
disableClick: PropTypes.bool, // Disallow clicking on the dropzone container to open file dialog
26+
27+
inputProps: PropTypes.object, // Pass additional attributes to the <input type="file"/> tag
28+
multiple: PropTypes.bool, // Allow dropping multiple files
29+
accept: PropTypes.string, // Allow specific types of files. See https://github.com/okonet/attr-accept for more information
30+
name: PropTypes.string // name attribute for the input tag
31+
};
32+
33+
34+
static defaultProps = {
35+
disablePreview: false,
36+
disableClick: false,
37+
multiple: true
38+
};
39+
40+
state = {
41+
isDragActive: false
42+
};
43+
44+
componentDidMount() {
45+
this.enterCounter = 0;
46+
}
47+
48+
onDragStart = (e) => {
49+
if (this.props.onDragStart) {
50+
this.props.onDragStart.call(this, e);
51+
}
52+
};
53+
54+
onDragEnter = (e) => {
55+
const {onDragEnter} = this.props;
56+
57+
e.preventDefault();
58+
59+
// Count the dropzone and any children that are entered.
60+
++this.enterCounter;
61+
62+
// This is tricky. During the drag even the dataTransfer.files is null
63+
// But Chrome implements some drag store, which is accesible via dataTransfer.items
64+
const dataTransferItems = e.dataTransfer && e.dataTransfer.items ? e.dataTransfer.items : [];
65+
66+
// Now we need to convert the DataTransferList to Array
67+
const allFilesAccepted = this.allFilesAccepted(Array.prototype.slice.call(dataTransferItems));
68+
69+
this.setState({
70+
isDragActive: allFilesAccepted,
71+
isDragReject: !allFilesAccepted
72+
});
73+
74+
if (onDragEnter) {
75+
onDragEnter.call(this, e);
76+
}
77+
};
78+
79+
onDragOver = (e) => {
80+
e.preventDefault();
81+
e.stopPropagation();
82+
return false;
83+
};
84+
85+
onDragLeave = (e) => {
86+
const {onDragLeave} = this.props;
87+
88+
e.preventDefault();
89+
90+
// Only deactivate once the dropzone and all children was left.
91+
if (--this.enterCounter > 0) {
92+
return;
93+
}
94+
95+
this.setState({
96+
isDragActive: false,
97+
isDragReject: false
98+
});
99+
100+
if (onDragLeave) {
101+
onDragLeave.call(this, e);
102+
}
103+
};
104+
105+
onDrop = (e) => {
106+
const {onDrop, onDropAccepted, onDropRejected} = this.props;
107+
108+
e.preventDefault();
109+
110+
// Reset the counter along with the drag on a drop.
111+
this.enterCounter = 0;
112+
113+
this.setState({
114+
isDragActive: false,
115+
isDragReject: false
116+
});
117+
118+
const droppedFiles = e.dataTransfer ? e.dataTransfer.files : e.target.files;
119+
const max = this.props.multiple ? droppedFiles.length : Math.min(droppedFiles.length, 1);
120+
const files = [];
121+
122+
for (let i = 0; i < max; i++) {
123+
const file = droppedFiles[i];
124+
// We might want to disable the preview creation to support big files
125+
if (!this.props.disablePreview) {
126+
file.preview = window.URL.createObjectURL(file);
127+
}
128+
129+
files.push(file);
130+
}
131+
132+
if (this.allFilesAccepted(files)) {
133+
if (onDrop) {
134+
onDrop.call(this, files, e);
135+
}
136+
137+
if (onDropAccepted) {
138+
onDropAccepted.call(this, files, e);
139+
}
140+
} else {
141+
if (onDropRejected) {
142+
onDropRejected.call(this, files, e);
143+
}
144+
}
145+
};
146+
147+
onClick = () => {
148+
if (!this.props.disableClick) {
149+
this.open();
150+
}
151+
};
152+
153+
allFilesAccepted(files) {
154+
return files.every(file => accepts(file, this.props.accept));
155+
}
156+
157+
open() {
158+
this.fileInputEl.value = null;
159+
this.fileInputEl.click();
160+
}
161+
162+
render() {
163+
const {
164+
accept,
165+
activeClassName,
166+
inputProps,
167+
multiple,
168+
name,
169+
rejectClassName,
170+
...rest
171+
} = this.props;
172+
173+
let {
174+
activeStyle,
175+
className,
176+
rejectStyle,
177+
style,
178+
...props // eslint-disable-line prefer-const
179+
} = rest;
180+
181+
const {isDragActive, isDragReject} = this.state;
182+
183+
className = className || '';
184+
185+
if (isDragActive && activeClassName) {
186+
className += ' ' + activeClassName;
187+
}
188+
if (isDragReject && rejectClassName) {
189+
className += ' ' + rejectClassName;
190+
}
191+
192+
if (!className && !style && !activeStyle && !rejectStyle) {
193+
style = {
194+
backgroundColor: "#fafafa",
195+
width: 'auto',
196+
borderWidth: 1,
197+
borderColor: '#757575',
198+
borderStyle: 'dashed',
199+
borderRadius: 2
200+
};
201+
activeStyle = {
202+
borderStyle: 'solid',
203+
backgroundColor: '#fafafa'
204+
};
205+
rejectStyle = {
206+
borderStyle: 'solid',
207+
backgroundColor: '#fafafa'
208+
};
209+
}
210+
211+
let appliedStyle;
212+
if (activeStyle && isDragActive) {
213+
appliedStyle = {
214+
...style,
215+
...activeStyle
216+
};
217+
} else if (rejectStyle && isDragReject) {
218+
appliedStyle = {
219+
...style,
220+
...rejectStyle
221+
};
222+
} else {
223+
appliedStyle = {
224+
...style
225+
};
226+
}
227+
228+
const inputAttributes = {
229+
accept,
230+
type: 'file',
231+
style: {display: 'none'},
232+
multiple: multiple,
233+
ref: el => this.fileInputEl = el, // eslint-disable-line
234+
onChange: this.onDrop
235+
};
236+
237+
if (name && name.length) {
238+
inputAttributes.name = name;
239+
}
240+
241+
// Remove custom properties before passing them to the wrapper div element
242+
const customProps = ['disablePreview', 'disableClick', 'onDropAccepted', 'onDropRejected'];
243+
const divProps = {...props};
244+
customProps.forEach(prop => delete divProps[prop]);
245+
246+
return (
247+
<div
248+
className={className}
249+
style={appliedStyle}
250+
{...divProps/* expand user provided props first so event handlers are never overridden */}
251+
onClick={this.onClick}
252+
onDragStart={this.onDragStart}
253+
onDragEnter={this.onDragEnter}
254+
onDragOver={this.onDragOver}
255+
onDragLeave={this.onDragLeave}
256+
onDrop={this.onDrop}
257+
>
258+
{this.props.children}
259+
<input
260+
{...inputProps/* expand user provided inputProps first so inputAttributes override them */}
261+
{...inputAttributes}
262+
multiple/>
263+
</div>
264+
);
265+
}
266+
}

0 commit comments

Comments
 (0)