diff --git a/.gitignore b/.gitignore index 6ac12083..d397107d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ lib coverage yarn.lock es +package-lock.json +tmp/ \ No newline at end of file diff --git a/README.md b/README.md index 0bb8844d..8cf57ac6 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ React.render(, container); |component | "div"|"span" | "span"| wrap component name | |supportServerRender | boolean | false| whether to support server render | |onReady | function | | only call when supportServerRender is true, upload is rendered completely | -|action| string | | form action url | +|action| string | function(file): string | Promise<string> | | form action url | |data| object/function(file) | | other data object to post or a function which returns a data object | |headers| object | {} | http headers to post, available in modern browsers | |accept | string | | input accept attribute | @@ -113,7 +113,8 @@ abort(file?: File) => void: abort the uploading file #### Download Popup Problem -In iframe uploader way, the content-type of response should be `text/plain` or `text/html`.[referense](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation) +In iframe uploader way, the content-type of response should be `text/plain` or `text/html`. [See more about +Content-Type Negotiation](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation) What's more, in iframe mode, the response's status should always be `200 OK`, otherwise you might get an `Access is denied` error in IE 8/9. diff --git a/examples/customRequest.html b/examples/customRequest.html new file mode 100644 index 00000000..48cdce85 --- /dev/null +++ b/examples/customRequest.html @@ -0,0 +1 @@ +placeholder diff --git a/examples/customRequest.js b/examples/customRequest.js new file mode 100644 index 00000000..9e9f8d2f --- /dev/null +++ b/examples/customRequest.js @@ -0,0 +1,83 @@ +/* eslint no-console:0 */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Upload from 'rc-upload'; +import axios from 'axios'; + +const uploadProps = { + action: '/upload.do', + multiple: false, + data: { a: 1, b: 2 }, + headers: { + Authorization: '$prefix $token', + }, + onStart(file) { + console.log('onStart', file, file.name); + }, + onSuccess(ret, file) { + console.log('onSuccess', ret, file.name); + }, + onError(err) { + console.log('onError', err); + }, + onProgress({ percent }, file) { + console.log('onProgress', `${percent}%`, file.name); + }, + customRequest({ + action, + data, + file, + filename, + headers, + onError, + onProgress, + onSuccess, + withCredentials, + }) { + // EXAMPLE: post form-data with 'axios' + const formData = new FormData(); + if (data) { + Object.keys(data).map(key => { + formData.append(key, data[key]); + }); + } + formData.append(filename, file); + + axios + .post(action, formData, { + withCredentials, + headers, + onUploadProgress: ({ total, loaded }) => { + onProgress({ percent: Math.round(loaded / total * 100).toFixed(2) }, file); + }, + }) + .then(({ data: response }) => { + onSuccess(response, file); + }) + .catch(onError); + + return { + abort() { + console.log('upload progress is aborted.'); + }, + }; + }, +}; + +const Test = () => { + return ( +
+
+ + + +
+
+ ); +}; + +ReactDOM.render(, document.getElementById('__react-content')); diff --git a/examples/directoryUpload.html b/examples/directoryUpload.html new file mode 100644 index 00000000..b3a42524 --- /dev/null +++ b/examples/directoryUpload.html @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/examples/directoryUpload.js b/examples/directoryUpload.js new file mode 100644 index 00000000..f710edd1 --- /dev/null +++ b/examples/directoryUpload.js @@ -0,0 +1,50 @@ +/* eslint no-console:0 */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import Upload from 'rc-upload'; + +class Test extends React.Component { + constructor(props) { + super(props); + this.uploaderProps = { + action: '/upload.do', + data: { a: 1, b: 2 }, + headers: { + Authorization: 'xxxxxxx', + }, + directory: true, + beforeUpload(file) { + console.log('beforeUpload', file.name); + }, + onStart: (file) => { + console.log('onStart', file.name); + // this.refs.inner.abort(file); + }, + onSuccess(file) { + console.log('onSuccess', file); + }, + onProgress(step, file) { + console.log('onProgress', Math.round(step.percent), file.name); + }, + onError(err) { + console.log('onError', err); + }, + }; + } + render() { + return (
+ +
+ 开始上传 +
+ +
); + } +} + +ReactDOM.render(, document.getElementById('__react-content')); diff --git a/examples/simple.js b/examples/simple.js index 3d7bc442..7cf2209c 100644 --- a/examples/simple.js +++ b/examples/simple.js @@ -78,8 +78,14 @@ class Test extends React.Component { height: 500, }} > - - 开始上传2 + + 开始上传2 + diff --git a/package.json b/package.json index 2f0d0902..25106bb8 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "coverage": "jest --coverage && cat ./coverage/lcov.info | coveralls" }, "devDependencies": { + "axios": "^0.18.0", "co-busboy": "^1.3.0", "coveralls": "^2.13.1", "expect.js": "0.3.x", @@ -53,8 +54,8 @@ "dependencies": { "babel-runtime": "6.x", "classnames": "^2.2.5", - "warning": "2.x", - "prop-types": "^15.5.7" + "prop-types": "^15.5.7", + "warning": "2.x" }, "jest": { "collectCoverageFrom": [ diff --git a/src/AjaxUploader.jsx b/src/AjaxUploader.jsx index 7f8f102d..43e653d2 100644 --- a/src/AjaxUploader.jsx +++ b/src/AjaxUploader.jsx @@ -6,6 +6,7 @@ import classNames from 'classnames'; import defaultRequest from './request'; import getUid from './uid'; import attrAccept from './attr-accept'; +import traverseFileTree from './traverseFileTree'; class AjaxUploader extends Component { static propTypes = { @@ -14,6 +15,7 @@ class AjaxUploader extends Component { prefixCls: PropTypes.string, className: PropTypes.string, multiple: PropTypes.bool, + directory: PropTypes.bool, disabled: PropTypes.bool, accept: PropTypes.string, children: PropTypes.any, @@ -22,6 +24,10 @@ class AjaxUploader extends Component { PropTypes.object, PropTypes.func, ]), + action: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func, + ]), headers: PropTypes.object, beforeUpload: PropTypes.func, customRequest: PropTypes.func, @@ -54,16 +60,21 @@ class AjaxUploader extends Component { } onFileDrop = e => { + e.preventDefault(); + if (e.type === 'dragover') { - e.preventDefault(); return; } - const files = Array.prototype.slice.call(e.dataTransfer.files).filter( + let files = Array.prototype.slice.call(e.dataTransfer.files).filter( file => attrAccept(file, this.props.accept) ); - this.uploadFiles(files); - e.preventDefault(); + if (this.props.directory) { + traverseFileTree(e.dataTransfer.items, this.uploadFiles); + } else { + files = e.dataTransfer.files; + this.uploadFiles(files); + } } componentDidMount() { @@ -75,7 +86,7 @@ class AjaxUploader extends Component { this.abort(); } - uploadFiles(files) { + uploadFiles = (files) => { const postFiles = Array.prototype.slice.call(files); postFiles.forEach((file) => { file.uid = getUid(); @@ -95,10 +106,9 @@ class AjaxUploader extends Component { before.then((processedFile) => { const processedFileType = Object.prototype.toString.call(processedFile); if (processedFileType === '[object File]' || processedFileType === '[object Blob]') { - this.post(processedFile); - } else { - this.post(file); + return this.post(processedFile); } + return this.post(file); }).catch(e => { console && console.log(e); // eslint-disable-line }); @@ -117,28 +127,36 @@ class AjaxUploader extends Component { if (typeof data === 'function') { data = data(file); } - const { uid } = file; - const request = props.customRequest || defaultRequest; - this.reqs[uid] = request({ - action: props.action, - filename: props.name, - file, - data, - headers: props.headers, - withCredentials: props.withCredentials, - onProgress: onProgress ? e => { - onProgress(e, file); - } : null, - onSuccess: (ret, xhr) => { - delete this.reqs[uid]; - props.onSuccess(ret, file, xhr); - }, - onError: (err, ret) => { - delete this.reqs[uid]; - props.onError(err, ret, file); - }, + new Promise(resolve => { + const { action } = props; + if (typeof action === 'function') { + return resolve(action(file)); + } + resolve(action); + }).then(action => { + const { uid } = file; + const request = props.customRequest || defaultRequest; + this.reqs[uid] = request({ + action, + filename: props.name, + file, + data, + headers: props.headers, + withCredentials: props.withCredentials, + onProgress: onProgress ? e => { + onProgress(e, file); + } : null, + onSuccess: (ret, xhr) => { + delete this.reqs[uid]; + props.onSuccess(ret, file, xhr); + }, + onError: (err, ret) => { + delete this.reqs[uid]; + props.onError(err, ret, file); + }, + }); + onStart(file); }); - onStart(file); } reset() { @@ -176,7 +194,7 @@ class AjaxUploader extends Component { render() { const { component: Tag, prefixCls, className, disabled, - style, multiple, accept, children, + style, multiple, accept, children, directory, } = this.props; const cls = classNames({ [prefixCls]: true, @@ -203,6 +221,8 @@ class AjaxUploader extends Component { key={this.state.uid} style={{ display: 'none' }} accept={accept} + directory={directory ? 'directory' : null} + webkitdirectory={directory ? 'webkitdirectory' : null} multiple={multiple} onChange={this.onChange} /> diff --git a/src/IframeUploader.jsx b/src/IframeUploader.jsx index e4f71cd4..871209b4 100644 --- a/src/IframeUploader.jsx +++ b/src/IframeUploader.jsx @@ -31,7 +31,10 @@ class IframeUploader extends Component { PropTypes.object, PropTypes.func, ]), - action: PropTypes.string, + action: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func, + ]), name: PropTypes.string, } @@ -142,7 +145,7 @@ class IframeUploader extends Component {
{ + const { action } = this.props; + if (typeof action === 'function') { + return resolve(action(file)); + } + resolve(action); + }).then(action => { + formNode.setAttribute('action', action); + formNode.submit(); + dataSpan.innerHTML = ''; + onStart(file); + }); } saveIframe = (node) => { diff --git a/src/Upload.jsx b/src/Upload.jsx index 003086d4..8cac9e9b 100644 --- a/src/Upload.jsx +++ b/src/Upload.jsx @@ -11,9 +11,13 @@ class Upload extends Component { component: PropTypes.string, style: PropTypes.object, prefixCls: PropTypes.string, - action: PropTypes.string, + action: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func, + ]), name: PropTypes.string, multipart: PropTypes.bool, + directory: PropTypes.bool, onError: PropTypes.func, onSuccess: PropTypes.func, onProgress: PropTypes.func, @@ -85,7 +89,7 @@ class Upload extends Component { return null; } const ComponentUploader = this.getComponent(); - return ; + return ; } } diff --git a/src/traverseFileTree.js b/src/traverseFileTree.js new file mode 100644 index 00000000..67d21b28 --- /dev/null +++ b/src/traverseFileTree.js @@ -0,0 +1,21 @@ +const traverseFileTree = (files, callback) => { + const _traverseFileTree = (item, path) => { + path = path || ''; + if (item.isFile) { + item.file(file => callback([file])); + } else if (item.isDirectory) { + const dirReader = item.createReader(); + + dirReader.readEntries((entries) => { + for (const entrieItem of entries) { + _traverseFileTree(entrieItem, `${path}${item.name}/`); + } + }); + } + }; + for (const file of files) { + _traverseFileTree(file.webkitGetAsEntry()); + } +}; + +export default traverseFileTree;