react-files
Advanced tools
Comparing version 1.1.1 to 2.0.0
{ | ||
"name": "react-files", | ||
"version": "1.1.1", | ||
"version": "2.0.0", | ||
"main": "src/Files.js", | ||
@@ -40,4 +40,8 @@ "description": "A file input (dropzone) management component for React", | ||
"babel-preset-stage-0": "6.0.15", | ||
"eslint": "1.10.3", | ||
"eslint-plugin-react": "3.6.2", | ||
"eslint": "3.4.0", | ||
"eslint-config-standard": "6.0.0", | ||
"eslint-config-standard-react": "4.0.0", | ||
"eslint-plugin-promise": "2.0.1", | ||
"eslint-plugin-react": "6.2.0", | ||
"eslint-plugin-standard": "2.0.0", | ||
"expect": "1.20.2", | ||
@@ -44,0 +48,0 @@ "expect-jsx": "2.6.0", |
128
README.md
@@ -14,3 +14,3 @@ react-files | ||
``` | ||
```bash | ||
npm install react-files --save | ||
@@ -21,2 +21,4 @@ ``` | ||
#### Basic | ||
```js | ||
@@ -28,7 +30,7 @@ import React from 'react' | ||
var FilesDemo = React.createClass({ | ||
onSubmit: function(files) { | ||
onFilesChange: function (files) { | ||
console.log(files) | ||
}, | ||
onError: function(error, file) { | ||
onFilesError: function (error, file) { | ||
console.log('error code ' + error.code + ': ' + error.message) | ||
@@ -41,8 +43,14 @@ }, | ||
<Files | ||
onSubmit={this.onSubmit} | ||
onError={this.onError} | ||
maxFiles={10} | ||
maxSize={10000000} | ||
minSize={1000} | ||
/> | ||
className='files-dropzone' | ||
onChange={this.onFilesChange} | ||
onError={this.onFilesError} | ||
accepts={['image/png', 'text/plain', 'audio/*']} | ||
multiple | ||
maxFiles={3} | ||
maxFileSize={10000000} | ||
minFileSize={0} | ||
clickable | ||
> | ||
Drop files here or click to upload | ||
</Files> | ||
</div> | ||
@@ -56,7 +64,20 @@ ) | ||
### Props | ||
#### Advanced | ||
See "Tinker" instructions below to run and view all examples. | ||
> `onSubmit(files)` - Function | ||
### Tinker | ||
``` | ||
git clone https://github.com/mother/react-files | ||
npm install | ||
npm start | ||
``` | ||
Then visit http://localhost:8000/ | ||
## Props | ||
`onChange(files)` - *Function* | ||
Perform work on files added when submit is clicked. | ||
@@ -66,8 +87,6 @@ | ||
> `onError(error, file)` - Function | ||
`onError(error, file)` - *Function* | ||
- `error.code` - Number | ||
- `error.message` - String | ||
`error.code` - Number | ||
`error.message` - String | ||
Perform work or notify the user when an error occurs. | ||
@@ -83,9 +102,11 @@ | ||
> `accepts` - Array of String | ||
`accepts` - *Array* of *String* | ||
Control what types of generic/specific MIME types, or specific extensions can be dropped/added. | ||
Control what types of generic/specific MIME types, can be dropped/added. | ||
Example | ||
> See full list of MIME types here: http://www.iana.org/assignments/media-types/media-types.xhtml | ||
Example: | ||
```js | ||
accepts={['image/*', 'video/mp4', 'audio/*', '.pdf', '.txt']} | ||
accepts={['image/*', 'video/mp4', 'audio/*']} | ||
``` | ||
@@ -95,4 +116,22 @@ | ||
> `maxFiles` - Number | ||
`multiple` - *Boolean* | ||
Default: `true` | ||
Allow multiple files | ||
--- | ||
`clickable` - *Boolean* | ||
Default: `true` | ||
Dropzone is clickable to open file browser. Disable for dropping only. | ||
--- | ||
`maxFiles` - *Number* | ||
Default: `Infinity` | ||
Maximum number of files allowed | ||
@@ -102,4 +141,6 @@ | ||
> `maxSize` - Number | ||
`maxFileSize` - *Number* | ||
Default: `Infinity` | ||
Maximum file size allowed (in bytes) | ||
@@ -109,4 +150,6 @@ | ||
> `minSize` - Number | ||
`minFileSize` - *Number* | ||
Default: `0` | ||
Minimum file size allowed (in bytes) | ||
@@ -116,29 +159,10 @@ | ||
### Styling | ||
`dropActiveClassName` - *String* | ||
Be sure to style your Files component, available selectors are (view `style.css`): | ||
- .files-container | ||
- .files-dropzone-outer | ||
- .files-dropzone | ||
- .files-dropzone:before | ||
- .files-dropzone-ondragenter | ||
- .files-buttons | ||
- .files-button-submit | ||
- .files-button-submit:before | ||
- .files-button-clear | ||
- .files-button-clear:before | ||
- .files-list | ||
- .files-list ul | ||
- .files-list li:last-child | ||
- .files-list-item | ||
- .files-list-item-content | ||
- .files-list-item-content-item | ||
- .files-list-item-content-item-1 | ||
- .files-list-item-content-item-2 | ||
- .files-list-item-preview | ||
- .files-list-item-preview-image | ||
- .files-list-item-preview-extension | ||
- .files-list-item-remove | ||
- .files-list-item-remove-image | ||
Default: `'files-dropzone-active'` | ||
Class added to the Files component when user is actively hovering over the dropzone with files selected. | ||
--- | ||
### Test (todo) | ||
@@ -150,12 +174,4 @@ | ||
### Tinker | ||
``` | ||
git clone https://github.com/mother/react-files | ||
npm install | ||
npm start | ||
``` | ||
### License | ||
MIT. Copyright (c) 2016 Jared Reich. |
220
src/Files.js
import React from 'react' | ||
class Files extends React.Component { | ||
constructor(props, context) { | ||
constructor (props, context) { | ||
super(props, context) | ||
this.onDrop = this.onDrop.bind(this) | ||
this.onSubmit = this.onSubmit.bind(this) | ||
this.onClear = this.onClear.bind(this) | ||
this.onDragEnter = this.onDragEnter.bind(this) | ||
this.onDragLeave = this.onDragLeave.bind(this) | ||
this.openFileChooser = this.openFileChooser.bind(this) | ||
@@ -18,3 +18,3 @@ | ||
onDrop(event) { | ||
onDrop (event) { | ||
event.preventDefault() | ||
@@ -25,3 +25,9 @@ this.onDragLeave(event) | ||
// then return to method | ||
const filesAdded = event.dataTransfer ? event.dataTransfer.files : event.target.files | ||
let filesAdded = event.dataTransfer ? event.dataTransfer.files : event.target.files | ||
// Multiple files dropped when not allowed | ||
if (this.props.multiple === false && filesAdded.length > 1) { | ||
filesAdded = [filesAdded[0]] | ||
} | ||
let files = [] | ||
@@ -32,4 +38,10 @@ for (let i = 0; i < filesAdded.length; i++) { | ||
// Assign file an id | ||
file.id = 'files-list-item-' + this.id++ | ||
file.id = 'files-' + this.id++ | ||
// Tell file it's own extension | ||
file.extension = this.fileExtension(file) | ||
// Tell file it's own readable size | ||
file.sizeReadable = this.fileSizeReadable(file.size) | ||
// Add preview, either image or file extension | ||
@@ -43,4 +55,3 @@ if (file.type && this.mimeTypeLeft(file.type) === 'image') { | ||
file.preview = { | ||
type: 'file', | ||
extension: this.fileExtension(file) | ||
type: 'file' | ||
} | ||
@@ -52,4 +63,4 @@ } | ||
this.onError({ | ||
code: 4, | ||
message: 'maximum file count reached' | ||
code: 4, | ||
message: 'maximum file count reached' | ||
}, file) | ||
@@ -59,10 +70,17 @@ break | ||
// If file is acceptable, unshift | ||
if (this.fileTypeAcceptable(file) && | ||
this.fileSizeAcceptable(file)) files.unshift(file) | ||
// If file is acceptable, push or replace | ||
if (this.fileTypeAcceptable(file) && this.fileSizeAcceptable(file)) { | ||
files.push(file) | ||
} | ||
} | ||
this.setState({ files: [...files, ...this.state.files] }) | ||
this.setState({ | ||
files: this.props.multiple === false | ||
? files | ||
: [...this.state.files, ...files] | ||
}, () => { | ||
this.props.onChange.call(this, this.state.files) | ||
}) | ||
} | ||
onDragOver(event) { | ||
onDragOver (event) { | ||
event.preventDefault() | ||
@@ -72,11 +90,13 @@ event.stopPropagation() | ||
onDragEnter(event) { | ||
event.target.className += ' files-dropzone-ondragenter' | ||
onDragEnter (event) { | ||
let el = document.getElementsByClassName(this.props.className)[0] | ||
el.className += ' ' + this.props.dropActiveClassName | ||
} | ||
onDragLeave(event) { | ||
event.target.className = event.target.className.replace(' files-dropzone-ondragenter', '') | ||
onDragLeave (event) { | ||
let el = document.getElementsByClassName(this.props.className)[0] | ||
el.className = el.className.replace(' ' + this.props.dropActiveClassName, '') | ||
} | ||
openFileChooser() { | ||
openFileChooser () { | ||
this.inputElement.value = null | ||
@@ -86,12 +106,5 @@ this.inputElement.click() | ||
removeFile(fileId) { | ||
this.setState({ | ||
files: this.state.files.filter(file => file.id !== fileId) | ||
}) | ||
} | ||
fileTypeAcceptable(file) { | ||
let accepts = this.props.accepts | ||
fileTypeAcceptable (file) { | ||
let accepts = this.props.accepts | ||
if (accepts) { | ||
if (accepts.indexOf(this.fileExtension(file)) !== -1) return true | ||
if (file.type) { | ||
@@ -115,4 +128,4 @@ let typeLeft = this.mimeTypeLeft(file.type) | ||
this.onError({ | ||
code: 1, | ||
message: file.name + ' is not a valid file type' | ||
code: 1, | ||
message: file.name + ' is not a valid file type' | ||
}, file) | ||
@@ -125,13 +138,13 @@ return false | ||
fileSizeAcceptable(file) { | ||
if (file.size > this.props.maxSize) { | ||
fileSizeAcceptable (file) { | ||
if (file.size > this.props.maxFileSize) { | ||
this.onError({ | ||
code: 2, | ||
message: file.name + ' is too large' | ||
code: 2, | ||
message: file.name + ' is too large' | ||
}, file) | ||
return false | ||
} else if (file.size < this.props.minSize) { | ||
} else if (file.size < this.props.minFileSize) { | ||
this.onError({ | ||
code: 3, | ||
message: file.name + ' is too small' | ||
code: 3, | ||
message: file.name + ' is too small' | ||
}, file) | ||
@@ -144,14 +157,14 @@ return false | ||
mimeTypeLeft(mime) { | ||
mimeTypeLeft (mime) { | ||
return mime.split('/')[0] | ||
} | ||
mimeTypeRight(mime) { | ||
mimeTypeRight (mime) { | ||
return mime.split('/')[1] | ||
} | ||
fileExtension(file) { | ||
fileExtension (file) { | ||
let extensionSplit = file.name.split('.') | ||
if (extensionSplit.length > 1) { | ||
return '.' + extensionSplit[extensionSplit.length - 1] | ||
return extensionSplit[extensionSplit.length - 1] | ||
} else { | ||
@@ -162,3 +175,3 @@ return 'none' | ||
fileSizeReadable(size) { | ||
fileSizeReadable (size) { | ||
if (size >= 1000000000) { | ||
@@ -175,23 +188,31 @@ return Math.ceil(size / 1000000000) + 'GB' | ||
onSubmit() { | ||
this.props.onSubmit.call(this, this.state.files) | ||
onError (error, file) { | ||
this.props.onError.call(this, error, file) | ||
} | ||
onError(error, file) { | ||
this.props.onError.call(this, error, file) | ||
removeFile (fileToRemove) { | ||
this.setState({ | ||
files: this.state.files.filter(file => file.id !== fileToRemove.id) | ||
}, () => { | ||
this.props.onChange.call(this, this.state.files) | ||
}) | ||
} | ||
onClear() { | ||
removeFiles () { | ||
this.setState({ | ||
files: [] | ||
}, () => { | ||
this.props.onChange.call(this, this.state.files) | ||
}) | ||
} | ||
render() { | ||
render () { | ||
const inputAttributes = { | ||
type: 'file', | ||
multiple: true, | ||
accept: this.props.accepts ? this.props.accepts.join() : '', | ||
multiple: this.props.multiple ? this.props.multiple : true, | ||
style: { display: 'none' }, | ||
ref: element => this.inputElement = element, | ||
ref: (element) => { | ||
this.inputElement = element | ||
}, | ||
onChange: this.onDrop | ||
@@ -201,53 +222,20 @@ } | ||
return ( | ||
<div | ||
className="files-container" | ||
> | ||
<div> | ||
<input | ||
// {...inputProps/* expand user provided inputProps first so inputAttributes override them */} | ||
{...inputAttributes} | ||
/> | ||
<div | ||
className="files-dropzone-outer" | ||
<div className={this.props.className} | ||
onClick={ | ||
this.props.clickable === true | ||
? this.openFileChooser | ||
: null | ||
} | ||
onDrop={this.onDrop} | ||
onDragOver={this.onDragOver} | ||
onDragEnter={this.onDragEnter} | ||
onDragLeave={this.onDragLeave} | ||
> | ||
<div className="files-dropzone" | ||
onClick={this.openFileChooser} | ||
onDrop={this.onDrop} | ||
onDragOver={this.onDragOver} | ||
onDragEnter={this.onDragEnter} | ||
onDragLeave={this.onDragLeave} | ||
/> | ||
{this.props.children} | ||
</div> | ||
{this.props.children} | ||
{ | ||
this.state.files.length > 0 | ||
? <div> | ||
<div className="files-list"> | ||
<ul>{this.state.files.map((file) => | ||
<li className="files-list-item" key={file.id}> | ||
<div className="files-list-item-preview"> | ||
{file.preview.type === 'image' | ||
? <img className="files-list-item-preview-image" src={file.preview.url} /> | ||
: <div className="files-list-item-preview-extension">{file.preview.extension}</div>} | ||
</div> | ||
<div className="files-list-item-content"> | ||
<div className="files-list-item-content-item files-list-item-content-item-1">{file.name}</div> | ||
<div className="files-list-item-content-item-2 files-list-item-content-item-2" className="files-list-item-content-item">{this.fileSizeReadable(file.size)}</div> | ||
</div> | ||
<div | ||
id={file.id} | ||
className="files-list-item-remove" | ||
onClick={this.removeFile.bind(this, file.id)} | ||
/> | ||
</li> | ||
)}</ul> | ||
</div> | ||
<div className="files-buttons"> | ||
<div onClick={this.onSubmit} className="files-button-submit" /> | ||
<div onClick={this.onClear} className="files-button-clear" /> | ||
</div> | ||
</div> | ||
: null | ||
} | ||
</div> | ||
) | ||
@@ -258,21 +246,33 @@ } | ||
Files.propTypes = { | ||
onSubmit: React.PropTypes.func.isRequired, | ||
onError: React.PropTypes.func.isRequired, | ||
children: React.PropTypes.oneOfType([ | ||
React.PropTypes.arrayOf(React.PropTypes.node), | ||
React.PropTypes.node | ||
]), | ||
className: React.PropTypes.string.isRequired, | ||
dropActiveClassName: React.PropTypes.string, | ||
onChange: React.PropTypes.func, | ||
onError: React.PropTypes.func, | ||
accepts: React.PropTypes.array, | ||
multiple: React.PropTypes.bool, | ||
maxFiles: React.PropTypes.number, | ||
maxSize: React.PropTypes.number, | ||
minSize: React.PropTypes.number | ||
maxFileSize: React.PropTypes.number, | ||
minFileSize: React.PropTypes.number, | ||
clickable: React.PropTypes.bool | ||
} | ||
Files.defaultProps = { | ||
onSubmit: function (files) { | ||
console.log(files) | ||
}, | ||
onError: function (error, file) { | ||
console.log('error code ' + error.code + ': ' + error.message) | ||
}, | ||
maxFiles: Infinity, | ||
maxSize: Infinity, | ||
minSize: 0 | ||
onChange: function (files) { | ||
console.log(files) | ||
}, | ||
onError: function (error, file) { | ||
console.log('error code ' + error.code + ': ' + error.message) | ||
}, | ||
dropActiveClassName: 'files-dropzone-active', | ||
multiple: true, | ||
maxFiles: Infinity, | ||
maxFileSize: Infinity, | ||
minFileSize: 0, | ||
clickable: true | ||
} | ||
export default Files |
131
src/index.js
@@ -5,21 +5,76 @@ import React from 'react' | ||
var FilesDemo = React.createClass({ | ||
onSubmit: function(files) { | ||
console.log(files) | ||
var FilesDemo1 = React.createClass({ | ||
getInitialState () { | ||
return { | ||
files: [] | ||
} | ||
}, | ||
onError: function(error, file) { | ||
onFilesChange: function (files) { | ||
this.setState({ | ||
files | ||
}, () => { | ||
console.log(this.state.files) | ||
}) | ||
}, | ||
onFilesError: function (error, file) { | ||
console.log('error code ' + error.code + ': ' + error.message) | ||
}, | ||
render: function() { | ||
filesRemoveOne: function (file) { | ||
this.refs.files.removeFile(file) | ||
}, | ||
filesRemoveAll: function () { | ||
this.refs.files.removeFiles() | ||
}, | ||
filesUpload: function () { | ||
window.alert('Ready to upload ' + this.state.files.length + ' file(s)!') | ||
}, | ||
render: function () { | ||
return ( | ||
<div className="files"> | ||
<div> | ||
<h1>Example 1 - List</h1> | ||
<Files | ||
onSubmit={this.onSubmit} | ||
onError={this.onError} | ||
ref='files' | ||
className='files-dropzone-list' | ||
onChange={this.onFilesChange} | ||
onError={this.onFilesError} | ||
multiple | ||
maxFiles={10} | ||
maxSize={10000000} | ||
minSize={1000} | ||
/> | ||
maxFileSize={10000000} | ||
minFileSize={0} | ||
clickable | ||
> | ||
Drop files here or click to upload | ||
</Files> | ||
<button onClick={this.filesRemoveAll}>Remove All Files</button> | ||
<button onClick={this.filesUpload}>Upload</button> | ||
{ | ||
this.state.files.length > 0 | ||
? <div className='files-list'> | ||
<ul>{this.state.files.map((file) => | ||
<li className='files-list-item' key={file.id}> | ||
<div className='files-list-item-preview'> | ||
{file.preview.type === 'image' | ||
? <img className='files-list-item-preview-image' src={file.preview.url} /> | ||
: <div className='files-list-item-preview-extension'>{file.extension}</div>} | ||
</div> | ||
<div className='files-list-item-content'> | ||
<div className='files-list-item-content-item files-list-item-content-item-1'>{file.name}</div> | ||
<div className='files-list-item-content-item files-list-item-content-item-2'>{file.sizeReadable}</div> | ||
</div> | ||
<div | ||
id={file.id} | ||
className='files-list-item-remove' | ||
onClick={this.filesRemoveOne.bind(this, file)} // eslint-disable-line | ||
/> | ||
</li> | ||
)}</ul> | ||
</div> | ||
: null | ||
} | ||
</div> | ||
@@ -30,2 +85,54 @@ ) | ||
ReactDOM.render(<FilesDemo />, document.getElementById('container')) | ||
var FilesDemo2 = React.createClass({ | ||
getInitialState () { | ||
return { | ||
files: [] | ||
} | ||
}, | ||
onFilesChange: function (files) { | ||
this.setState({ | ||
files | ||
}, () => { | ||
console.log(this.state.files) | ||
}) | ||
}, | ||
onFilesError: function (error, file) { | ||
console.log('error code ' + error.code + ': ' + error.message) | ||
}, | ||
filesRemoveAll: function () { | ||
this.refs.files.removeFiles() | ||
}, | ||
render: function () { | ||
return ( | ||
<div> | ||
<h1>Example 2 - Gallery</h1> | ||
<Files | ||
ref='files' | ||
className='files-dropzone-gallery' | ||
onChange={this.onFilesChange} | ||
onError={this.onFilesError} | ||
accepts={['image/*']} | ||
multiple | ||
clickable={false} | ||
> | ||
{ | ||
this.state.files.length > 0 | ||
? <div className='files-gallery'> | ||
{this.state.files.map((file) => | ||
<img className='files-gallery-item' src={file.preview.url} key={file.id} /> | ||
)} | ||
</div> | ||
: <div>Drop images here</div> | ||
} | ||
</Files> | ||
<button onClick={this.filesRemoveAll}>Remove All Files</button> | ||
</div> | ||
) | ||
} | ||
}) | ||
ReactDOM.render(<div><FilesDemo1 /><FilesDemo2 /></div>, document.getElementById('container')) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
781242
550
166
20