37 |
Example 1 - List
38 |
48 | Drop files here or click to upload
49 |
50 |
51 |
52 |
53 | {files.length > 0 && (
54 |
77 | )
78 | }
79 |
80 | export default ListWithUpload
81 |
--------------------------------------------------------------------------------
/examples/RenderProps.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Files from '../src'
3 |
4 | const RenderProps = () => (
5 |
6 |
Example 3 - Render Props
7 |
11 | {isDragging => (
12 |
19 | {isDragging && 'Drop Here!'}
20 | {!isDragging && 'Ready'}
21 |
22 | )}
23 |
24 |
25 | )
26 |
27 | export default RenderProps
28 |
--------------------------------------------------------------------------------
/examples/assets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Roboto', sans-serif;
3 | }
4 |
5 |
6 | /* LIST */
7 | /* ================== */
8 |
9 | .files-dropzone-list {
10 | display: table-cell;
11 | vertical-align: middle;
12 | text-align: center;
13 | padding: 10px;
14 | border: 1px dashed #D3D3D3;
15 | cursor: pointer;
16 | }
17 |
18 | .files-buttons {
19 | border-top: 1px solid #D3D3D3;
20 | cursor: pointer;
21 | }
22 | .files-button-submit {
23 | display: inline-block;
24 | text-align: center;
25 | height: 40px;
26 | line-height: 40px;
27 | width: 50%;
28 | box-sizing: border-box;
29 | border-right: 1px solid #D3D3D3;
30 | }
31 | .files-button-submit:before {
32 | content: "Submit"
33 | }
34 | .files-button-clear {
35 | display: inline-block;
36 | text-align: center;
37 | height: 40px;
38 | line-height: 40px;
39 | width: 50%;
40 | box-sizing: border-box;
41 | }
42 | .files-button-clear:before {
43 | content: "Clear"
44 | }
45 | .files-list {
46 | width: 300px;
47 | }
48 | .files-list ul {
49 | list-style: none;
50 | margin: 0;
51 | padding: 0;
52 | }
53 | .files-list li:last-child {
54 | border: none;
55 | }
56 | .files-list-item {
57 | height: 60px;
58 | padding: 10px 0px 10px 10px;
59 | /*border-bottom: 1px solid #D3D3D3;*/
60 | }
61 | .files-list-item-content {
62 | float: left;
63 | padding-top: 5px;
64 | padding-left: 10px;
65 | width: calc(100% - 130px);
66 | }
67 | .files-list-item-content-item {
68 | overflow: hidden;
69 | white-space: nowrap;
70 | text-overflow: ellipsis;
71 | }
72 | .files-list-item-content-item-1 {
73 | font-size: 20px;
74 | line-height: 30px;
75 | }
76 | .files-list-item-content-item-2 {
77 | font-size: 16px;
78 | line-height: 20px;
79 | }
80 | .files-list-item-preview {
81 | height: 60px;
82 | width: 60px;
83 | float: left;
84 | }
85 | .files-list-item-preview-image {
86 | height: 100%;
87 | width: 100%;
88 | }
89 | .files-list-item-preview-extension {
90 | text-align: center;
91 | line-height: 60px;
92 | color: #FFF;
93 | background-color: #D3D3D3;
94 | font-size: 16px;
95 | overflow: hidden;
96 | white-space: nowrap;
97 | text-overflow: ellipsis;
98 | padding-left: 5px;
99 | padding-right: 5px;
100 | box-sizing: border-box;
101 | }
102 | .files-list-item-remove {
103 | height: 60px;
104 | width: 60px;
105 | float: right;
106 | cursor: pointer;
107 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDQ4IDQ4IiB3aWR0aD0iNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTM4IDEyLjgzbC0yLjgzLTIuODMtMTEuMTcgMTEuMTctMTEuMTctMTEuMTctMi44MyAyLjgzIDExLjE3IDExLjE3LTExLjE3IDExLjE3IDIuODMgMi44MyAxMS4xNy0xMS4xNyAxMS4xNyAxMS4xNyAyLjgzLTIuODMtMTEuMTctMTEuMTd6Ii8+PHBhdGggZD0iTTAgMGg0OHY0OGgtNDh6IiBmaWxsPSJub25lIi8+PC9zdmc+) no-repeat center center;
108 | background-size: 30px 30px;
109 | }
110 | .files-list-item-remove-image {
111 | height: 30px;
112 | width: 30px;
113 | margin-top: 15px;
114 | margin-right: 10px;
115 | float: right;
116 | }
117 |
118 |
119 |
120 | /* GALLERY */
121 | /* ================== */
122 |
123 | .files-dropzone-gallery {
124 | padding: 10px;
125 | border: 1px dashed #D3D3D3;
126 | min-height: 300px;
127 | width: 500px;
128 | }
129 | .files-gallery-item {
130 | height: 80px;
131 | margin: 5px;
132 | }
133 |
134 |
135 | .files-dropzone-active {
136 | border: 1px solid lightgreen;
137 | color: lightgreen;
138 | }
139 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
react-files
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import ListWithUploadExample from './ListWithUpload'
4 | import GalleryExample from './Gallery'
5 | import RenderPropsExample from './RenderProps'
6 |
7 | const container = document.getElementById('container')
8 | const root = createRoot(container)
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 | )
16 |
--------------------------------------------------------------------------------
/examples/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const fs = require('fs')
3 | const multer = require('multer')
4 | const path = require('path')
5 | const webpack = require('webpack')
6 | const webpackDevMiddleware = require('webpack-dev-middleware')
7 | const webpackHotMiddleware = require('webpack-hot-middleware')
8 | const webpackConfig = require('../config/webpack.dev.config')
9 |
10 | const compiler = webpack(webpackConfig)
11 | const app = express()
12 | const upload = multer({ dest: path.join(__dirname, '.uploads') })
13 |
14 | app.use(webpackDevMiddleware(compiler, {
15 | publicPath: webpackConfig.output.publicPath
16 | }))
17 |
18 | app.use(webpackHotMiddleware(compiler, {
19 | log: console.log, // eslint-disable-line no-console
20 | path: '/__webpack_hmr',
21 | heartbeat: 10 * 1000
22 | }))
23 |
24 | app.use('/assets', express.static(path.join(__dirname, 'assets')))
25 |
26 | app.get('/', (req, res, next) => {
27 | res.sendFile(path.join(`${__dirname}/index.html`))
28 | })
29 |
30 | app.post('/files', upload.any(), (req, res, next) => {
31 | if (!req.files) {
32 | return next(new Error('No files uploaded'))
33 | }
34 |
35 | req.files.forEach((file) => {
36 | console.log(file) // eslint-disable-line no-console
37 | fs.unlinkSync(file.path)
38 | })
39 |
40 | res.status(200).end()
41 | })
42 |
43 | const server = app.listen(process.env.PORT || 8080, () => {
44 | // eslint-disable-next-line no-console
45 | console.log(`Starting react-files examples on port ${server.address().port}`)
46 | })
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-files",
3 | "version": "3.0.3",
4 | "main": "dist/index.js",
5 | "description": "A file input (dropzone) management component for React",
6 | "scripts": {
7 | "build": "rm -rf dist && webpack --config config/webpack.build.config.js",
8 | "examples": "node examples/server.js",
9 | "lint": "git diff HEAD --name-only --diff-filter=ACM | grep '.js$' | xargs node ./node_modules/eslint/bin/eslint.js --quiet",
10 | "lint-full": "node ./node_modules/eslint/bin/eslint.js .",
11 | "webpack-analyze": "mkdir -p .tmp; NODE_ENV=production webpack --config config/webpack.build.config.js --profile --json > .tmp/stats.json; webpack-bundle-analyzer .tmp/stats.json"
12 | },
13 | "husky": {
14 | "hooks": {
15 | "pre-commit": "npm run lint && npm audit",
16 | "post-merge": "npm install",
17 | "post-receive": "npm install",
18 | "post-rewrite": "npm install"
19 | }
20 | },
21 | "files": [
22 | "dist/index.js"
23 | ],
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/mother/react-files.git"
27 | },
28 | "keywords": [
29 | "react",
30 | "reactjs",
31 | "component",
32 | "file",
33 | "files",
34 | "input",
35 | "dropzone"
36 | ],
37 | "author": "Mother Co",
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/mother/react-files/issues"
41 | },
42 | "homepage": "https://github.com/mother/react-files",
43 | "peerDependencies": {
44 | "react": ">=16.8",
45 | "react-dom": ">=16.8"
46 | },
47 | "devDependencies": {
48 | "@babel/core": "^7.24.7",
49 | "@babel/polyfill": "^7.12.1",
50 | "@babel/preset-env": "^7.20.2",
51 | "@babel/preset-react": "^7.18.6",
52 | "axios": "^1.7.2",
53 | "babel-eslint": "^10.1.0",
54 | "babel-loader": "^9.1.2",
55 | "blob": "^0.1.0",
56 | "eslint-config-mother": "^2.0.15",
57 | "express": "^4.19.2",
58 | "form-data": "^4.0.0",
59 | "husky": "^4.2.5",
60 | "multer": "^1.4.5-lts.1",
61 | "prop-types": "^15.8.1",
62 | "react": "^18.2.0",
63 | "react-dom": "^18.2.0",
64 | "webpack": "^5.92.1",
65 | "webpack-bundle-analyzer": "^4.7.0",
66 | "webpack-cli": "^5.0.1",
67 | "webpack-dev-middleware": "^6.1.3",
68 | "webpack-hot-middleware": "^2.26.1"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useRef, useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import fileExtension from './utils/fileExtension'
4 | import fileSizeReadable from './utils/fileSizeReadable'
5 | import fileTypeAcceptable from './utils/fileTypeAcceptable'
6 |
7 | const Files = ({
8 | accepts = null,
9 | children,
10 | className,
11 | clickable = true,
12 | dragActiveClassName,
13 | inputProps = {},
14 | multiple = true,
15 | maxFiles = Infinity,
16 | maxFileSize = Infinity,
17 | minFileSize = 0,
18 | name = 'file',
19 | onChange = files => console.log(files), // eslint-disable-line no-console
20 | onDragEnter,
21 | onDragLeave,
22 | onError = err => console.log(`error code ${err.code}: ${err.message}`), // eslint-disable-line no-console
23 | style
24 | }) => {
25 | const idCounter = useRef(1)
26 | const dropzoneElement = useRef()
27 | const inputElement = useRef()
28 | const [isDragging, setDragging] = useState(false)
29 |
30 | const handleError = (error, file) => {
31 | onError(error, file)
32 | }
33 |
34 | const handleDragOver = useCallback((event) => {
35 | event.preventDefault()
36 | event.stopPropagation()
37 | }, [])
38 |
39 | const handleDragEnter = (event) => {
40 | const el = dropzoneElement.current
41 | if (dragActiveClassName && !el.className.includes(dragActiveClassName)) {
42 | el.className = `${el.className} ${dragActiveClassName}`
43 | }
44 |
45 | if (typeof children === 'function') {
46 | setDragging(true)
47 | }
48 |
49 | if (onDragEnter) {
50 | onDragEnter(event)
51 | }
52 | }
53 |
54 | const handleDragLeave = (event) => {
55 | const el = dropzoneElement.current
56 | if (dragActiveClassName) {
57 | el.className = el.className.replace(` ${dragActiveClassName}`, '')
58 | }
59 |
60 | if (typeof children === 'function') {
61 | setDragging(false)
62 | }
63 |
64 | if (onDragLeave) {
65 | onDragLeave(event)
66 | }
67 | }
68 |
69 | const openFileChooser = () => {
70 | inputElement.current.value = null
71 | inputElement.current.click()
72 | }
73 |
74 | const handleDrop = (event) => {
75 | event.preventDefault()
76 | handleDragLeave(event)
77 |
78 | // Collect added files, perform checking, cast pseudo-array to Array,
79 | // then return to method
80 | let filesAdded = event.dataTransfer
81 | ? event.dataTransfer.files
82 | : event.target.files
83 |
84 | // Multiple files dropped when not allowed
85 | if (multiple === false && filesAdded.length > 1) {
86 | filesAdded = [filesAdded[0]]
87 | }
88 |
89 | const fileResults = []
90 | for (let i = 0; i < filesAdded.length; i += 1) {
91 | const file = filesAdded[i]
92 |
93 | // Assign file an id
94 | file.id = `files-${idCounter.current}`
95 | idCounter.current += 1
96 |
97 | // Tell file it's own extension
98 | file.extension = fileExtension(file)
99 |
100 | // Tell file it's own readable size
101 | file.sizeReadable = fileSizeReadable(file.size)
102 |
103 | // Add preview, either image or file extension
104 | if (file.type && file.type.split('/')[0] === 'image') {
105 | file.preview = {
106 | type: 'image',
107 | url: window.URL.createObjectURL(file)
108 | }
109 | } else {
110 | file.preview = {
111 | type: 'file'
112 | }
113 | }
114 |
115 | // Check max file count
116 | if (fileResults.length >= maxFiles) {
117 | handleError({
118 | code: 4,
119 | message: 'maximum file count reached'
120 | }, file)
121 |
122 | break
123 | }
124 |
125 | // Check if file is too big
126 | if (file.size > maxFileSize) {
127 | handleError({
128 | code: 2,
129 | message: `${file.name} is too large`
130 | }, file)
131 |
132 | break
133 | }
134 |
135 | // Check if file is too small
136 | if (file.size < minFileSize) {
137 | handleError({
138 | code: 3,
139 | message: `${file.name} is too small`
140 | }, file)
141 |
142 | break
143 | }
144 |
145 | // Ensure acceptable file type
146 | if (!fileTypeAcceptable(accepts, file)) {
147 | handleError({
148 | code: 1,
149 | message: `${file.name} is not a valid file type`
150 | }, file)
151 |
152 | break
153 | }
154 |
155 | fileResults.push(file)
156 | }
157 |
158 | onChange(fileResults)
159 | }
160 |
161 | return (
162 | <>
163 |
173 | {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
174 |
183 | {typeof children === 'function' ? children(isDragging) : children}
184 |
185 | >
186 | )
187 | }
188 |
189 | Files.propTypes = {
190 | accepts: PropTypes.array,
191 | children: PropTypes.oneOfType([
192 | PropTypes.func,
193 | PropTypes.arrayOf(PropTypes.node),
194 | PropTypes.node
195 | ]),
196 | className: PropTypes.string,
197 | clickable: PropTypes.bool,
198 | dragActiveClassName: PropTypes.string,
199 | inputProps: PropTypes.object,
200 | multiple: PropTypes.bool,
201 | maxFiles: PropTypes.number,
202 | maxFileSize: PropTypes.number,
203 | minFileSize: PropTypes.number,
204 | name: PropTypes.string,
205 | onChange: PropTypes.func,
206 | onDragEnter: PropTypes.func,
207 | onDragLeave: PropTypes.func,
208 | onError: PropTypes.func,
209 | style: PropTypes.object
210 | }
211 |
212 | export default Files
213 |
--------------------------------------------------------------------------------
/src/utils/fileExtension.js:
--------------------------------------------------------------------------------
1 | const fileExtension = (file) => {
2 | const extensionSplit = file.name.split('.')
3 | if (extensionSplit.length > 1) {
4 | return extensionSplit[extensionSplit.length - 1]
5 | }
6 |
7 | return 'none'
8 | }
9 |
10 | export default fileExtension
11 |
--------------------------------------------------------------------------------
/src/utils/fileSizeReadable.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-template */
2 | const fileSizeReadable = (size) => {
3 | if (size >= 1000000000) {
4 | return Math.ceil(size / 1000000000) + 'GB'
5 | }
6 |
7 | if (size >= 1000000) {
8 | return Math.ceil(size / 1000000) + 'MB'
9 | }
10 |
11 | if (size >= 1000) {
12 | return Math.ceil(size / 1000) + 'KB'
13 | }
14 |
15 | return Math.ceil(size) + 'B'
16 | }
17 | /* eslint-enable prefer-template */
18 |
19 | export default fileSizeReadable
20 |
--------------------------------------------------------------------------------
/src/utils/fileTypeAcceptable.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | const mimeTypeRegexp = /^(application|audio|example|image|message|model|multipart|text|video|\*)\/[a-z0-9\.\+\*-]+$/
3 | const extRegexp = /\.[a-zA-Z0-9]*$/
4 |
5 | const fileTypeAcceptable = (accepts, file) => {
6 | if (!accepts) {
7 | return true
8 | }
9 |
10 | return accepts.some((accept) => {
11 | if (file.type && accept.match(mimeTypeRegexp)) {
12 | const [typeLeft, typeRight] = file.type.split('/')
13 | const [acceptLeft, acceptRight] = accept.split('/')
14 |
15 | if (acceptLeft && acceptRight) {
16 | if (acceptLeft === '*' && acceptRight === '*') {
17 | return true
18 | }
19 |
20 | if (acceptLeft === typeLeft && acceptRight === '*') {
21 | return true
22 | }
23 |
24 | if (acceptLeft === typeLeft && acceptRight === typeRight) {
25 | return true
26 | }
27 | }
28 | } else if (file.extension && accept.match(extRegexp)) {
29 | const ext = accept.substr(1)
30 | return file.extension.toLowerCase() === ext.toLowerCase()
31 | }
32 |
33 | return false
34 | })
35 | }
36 |
37 | export default fileTypeAcceptable
38 |
--------------------------------------------------------------------------------