react-xhr-uploader-emondo
Version:
ReactJS component for XHR (level 2) file upload
547 lines (500 loc) • 18.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
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; }; }();
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _ReactCSSTransitionGroup = require('react/lib/ReactCSSTransitionGroup');
var _ReactCSSTransitionGroup2 = _interopRequireDefault(_ReactCSSTransitionGroup);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
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; }
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; }
var defaultStyles = {
root: {
border: '1px solid #CACACA',
padding: 20
},
dropTargetStyle: {
border: '3px dashed #4A90E2',
padding: 10,
backgroundColor: '#ffffff',
cursor: 'pointer'
},
dropTargetActiveStyle: {
backgroundColor: '#ccffcc'
},
placeHolderStyle: {
paddingLeft: '20%',
paddingRight: '20%',
textAlign: 'center'
},
uploadButtonStyle: {
width: '100%',
marginTop: 10,
height: 32,
alignSelf: 'center',
cursor: 'pointer',
backgroundColor: '#D9EBFF',
border: '1px solid #5094E3',
fontSize: 14
},
fileset: {
marginTop: 10,
paddingTop: 10,
paddingBottom: 10,
borderTop: '1px solid #CACACA'
},
fileDetails: {
paddingTop: 10,
display: 'flex',
alignItems: 'flex-start'
},
fileName: {
flexGrow: '8'
},
fileSize: {
'float': 'right',
flexGrow: '2',
alignSelf: 'flex-end'
},
removeButton: {
alignSelf: 'flex-end'
},
progress: {
marginTop: 10,
width: '100%',
height: 16,
WebkitAppearance: 'none'
}
};
var XHRUploader = function (_React$Component) {
_inherits(XHRUploader, _React$Component);
_createClass(XHRUploader, null, [{
key: 'propTypes',
get: function get() {
return {
url: _react2.default.PropTypes.string.isRequired,
auto: _react2.default.PropTypes.bool,
fieldName: _react2.default.PropTypes.string,
buttonLabel: _react2.default.PropTypes.string,
dropzoneLabel: _react2.default.PropTypes.string,
maxSize: _react2.default.PropTypes.number,
chunks: _react2.default.PropTypes.bool,
chunkSize: _react2.default.PropTypes.number,
localStore: _react2.default.PropTypes.bool,
maxFiles: _react2.default.PropTypes.number,
encrypt: _react2.default.PropTypes.bool,
clearTimeOut: _react2.default.PropTypes.number,
filesetTransitionName: _react2.default.PropTypes.string,
styles: _react2.default.PropTypes.object,
debug: _react2.default.PropTypes.bool,
completionHandler: _react2.default.PropTypes.func
};
}
}, {
key: 'defaultProps',
get: function get() {
return {
auto: false,
fieldName: 'datafile',
buttonLabel: 'Upload',
dropzoneLabel: 'Drag and drop your files here or pick them from your computer',
maxSize: 25 * 1024 * 1024,
chunks: false,
chunkSize: 512 * 1024,
localStore: false,
maxFiles: 1,
encrypt: false,
clearTimeOut: 3000,
filesetTransitionName: 'fileset',
styles: defaultStyles,
debug: false
};
}
}]);
function XHRUploader(props) {
_classCallCheck(this, XHRUploader);
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(XHRUploader).call(this, props));
_this.state = { items: [] };
_this.activeDrag = 0;
_this.xhrs = [];
return _this;
}
_createClass(XHRUploader, [{
key: 'onClick',
value: function onClick() {
this.refs.fileInput.click();
}
}, {
key: 'onUploadButtonClick',
value: function onUploadButtonClick() {
this.upload();
}
}, {
key: 'onFileSelect',
value: function onFileSelect() {
var _this2 = this;
var items = this.filesToItems(this.refs.fileInput.files);
this.setState({ items: items }, function () {
if (_this2.props.auto) {
_this2.upload();
}
});
}
}, {
key: 'onDragEnter',
value: function onDragEnter(e) {
e.preventDefault();
this.activeDrag += 1;
this.setState({ isActive: this.activeDrag > 0 });
}
}, {
key: 'onDragOver',
value: function onDragOver(e) {
e.preventDefault();
e.stopPropagation();
return false;
}
}, {
key: 'onDragLeave',
value: function onDragLeave(e) {
e.preventDefault();
this.activeDrag -= 1;
if (this.activeDrag === 0) {
this.setState({ isActive: false });
}
}
}, {
key: 'onDrop',
value: function onDrop(e) {
var _this3 = this;
e.preventDefault();
this.activeDrag = 0;
this.setState({ isActive: false });
var droppedFiles = e.dataTransfer ? e.dataTransfer.files : [];
var items = this.filesToItems(droppedFiles);
this.setState({ items: items }, function () {
if (_this3.props.auto) {
_this3.upload();
}
});
}
}, {
key: 'clearIfAllCompleted',
value: function clearIfAllCompleted() {
var _this4 = this;
if (this.props.clearTimeOut > 0) {
var completed = this.state.items.filter(function (item) {
return item.progress === 100;
}).length;
if (completed === this.state.items.length) {
setTimeout(function () {
var items = _this4.state.items;
for (var itemIndex = 0; itemIndex < items.length; itemIndex++) {
_this4.props.completionHandler(items[itemIndex].response);
}
_this4.setState({ items: [] });
}, this.props.clearTimeOut);
}
}
}
}, {
key: 'updateFileProgress',
value: function updateFileProgress(index, progress) {
var newItems = [].concat(_toConsumableArray(this.state.items));
newItems[index] = Object.assign({}, this.state.items[index], { progress: progress });
this.setState({ items: newItems }, this.clearIfAllCompleted);
}
}, {
key: 'updateFileChunkProgress',
value: function updateFileChunkProgress(index, chunkIndex, progress) {
var newItems = [].concat(_toConsumableArray(this.state.items));
var currentItem = this.state.items[index];
var newProgressArr = [].concat(_toConsumableArray(currentItem.chunkProgress));
var totalProgress = newProgressArr.reduce(function (a, b) {
return a + b;
}) / newProgressArr.length;
newProgressArr[chunkIndex] = progress;
newItems[index] = Object.assign({}, currentItem, { chunkProgress: newProgressArr, progress: totalProgress });
this.setState({ items: newItems }, this.clearIfAllCompleted);
}
}, {
key: 'cancelFile',
value: function cancelFile(index) {
var newItems = [].concat(_toConsumableArray(this.state.items));
newItems[index] = Object.assign({}, this.state.items[index], { cancelled: true });
if (this.xhrs[index]) {
this.xhrs[index].upload.removeEventListener('progress');
this.xhrs[index].removeEventListener('load');
this.xhrs[index].abort();
}
this.setState({ items: newItems });
}
}, {
key: 'upload',
value: function upload() {
var _this5 = this;
var items = this.state.items;
if (items) {
items.filter(function (item) {
return !item.cancelled;
}).forEach(function (item) {
_this5.uploadItem(item);
});
}
}
}, {
key: 'uploadItem',
value: function uploadItem(item) {
var _this6 = this;
if (this.props.chunks) {
var BYTES_PER_CHUNK = this.props.chunkSize;
var SIZE = item.file.size;
var start = 0;
var end = BYTES_PER_CHUNK;
var chunkProgressHandler = function chunkProgressHandler(percentage, chunkIndex) {
_this6.updateFileChunkProgress(item.index, chunkIndex, percentage);
};
var chunkIndex = 0;
while (start < SIZE) {
this.uploadChunk(item, start, end, chunkIndex++, item.file.name, chunkProgressHandler);
start = end;
end = start + BYTES_PER_CHUNK;
}
} else {
this.uploadFile(item, function (progress) {
_this6.updateFileProgress(item.index, progress);
});
}
}
}, {
key: 'receiveResult',
value: function receiveResult(xhr, index) {
var _this7 = this;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var result = JSON.parse(xhr.responseText);
if (result) {
var newItems = [].concat(_toConsumableArray(_this7.state.items));
var currentItem = _this7.state.items[index];
newItems[index] = Object.assign({}, currentItem, { response: result });
_this7.setState({ items: newItems });
}
}
};
}
}, {
key: 'uploadChunk',
value: function uploadChunk(item, start, end, chunkIndex, fileName, progressCallback) {
if (item) {
var blob = item.file.slice(start, end);
var formData = new FormData();
var xhr = new XMLHttpRequest();
formData.append(this.props.fieldName, blob, fileName + '-chunk' + chunkIndex);
xhr.onload = function () {
progressCallback(100, chunkIndex);
};
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
progressCallback(e.loaded / e.total * 100, chunkIndex);
}
};
this.receiveResult(xhr, item.index);
xhr.open('POST', this.props.url, true);
xhr.send(formData);
}
}
}, {
key: 'uploadFile',
value: function uploadFile(item, progressCallback) {
if (item) {
var formData = new FormData();
var xhr = new XMLHttpRequest();
var file = item.file;
formData.append(this.props.fieldName, file, file.name);
xhr.onload = function () {
progressCallback(100);
};
this.receiveResult(xhr, item.index);
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
progressCallback(e.loaded / e.total * 100);
}
};
xhr.open('POST', this.props.url, true);
xhr.send(formData);
this.xhrs[file.index] = xhr;
}
}
}, {
key: 'filesToItems',
value: function filesToItems(files) {
var _this8 = this;
var fileItems = Array.prototype.slice.call(files).slice(0, this.props.maxFiles);
var items = fileItems.map(function (f, i) {
if (_this8.props.chunks) {
var chunkProgress = [];
for (var j = 0; j <= f.size / _this8.props.chunkSize; j++) {
chunkProgress.push(0);
}
return { file: f, index: i, progress: 0, cancelled: false, chunkProgress: chunkProgress, response: undefined };
}
return { file: f, index: i, progress: 0, cancelled: false, response: undefined };
});
return items;
}
}, {
key: 'renderDropTarget',
value: function renderDropTarget() {
var _this9 = this;
var styles = this.props.styles;
var dropTargetStyle = styles.dropTargetStyle;
if (this.state.isActive) {
dropTargetStyle = Object.assign({}, dropTargetStyle, styles.dropTargetActiveStyle);
}
return _react2.default.createElement(
'div',
{ ref: 'dropTarget', style: dropTargetStyle,
onClick: function onClick(e) {
return _this9.onClick(e);
},
onDragEnter: function onDragEnter(e) {
return _this9.onDragEnter(e);
},
onDragOver: function onDragOver(e) {
return _this9.onDragOver(e);
},
onDragLeave: function onDragLeave(e) {
return _this9.onDragLeave(e);
},
onDrop: function onDrop(e) {
return _this9.onDrop(e);
}
},
_react2.default.createElement(
'div',
{ style: styles.placeHolderStyle },
_react2.default.createElement(
'p',
null,
this.props.dropzoneLabel
),
_react2.default.createElement('center', { className: 'icon-upload icon-large' })
)
);
}
}, {
key: 'renderFileSet',
value: function renderFileSet() {
var _this10 = this;
var items = this.state.items;
var transitionName = this.props.filesetTransitionName;
if (items.length > 0) {
var _ret = function () {
var styles = _this10.props.styles;
var progress = _this10.state.progress;
return {
v: _react2.default.createElement(
_ReactCSSTransitionGroup2.default,
{ component: 'div', transitionName: transitionName, transitionEnterTimeout: 0, transitionLeaveTimeout: 0 },
_react2.default.createElement(
'div',
{ style: styles.fileset },
items.filter(function (item) {
return !item.cancelled;
}).map(function (item) {
var file = item.file;
var sizeInMB = (file.size / (1024 * 1024)).toPrecision(2);
var actionButtonClass = item.progress < 100 ? 'icon-cancel-circle icon-button icon-red' : 'icon-checkmark icon-button icon-green';
return _react2.default.createElement(
'div',
{ key: item.index },
_react2.default.createElement(
'div',
{ style: styles.fileDetails },
_react2.default.createElement(
'span',
{ className: 'icon-file icon-large' },
' '
),
_react2.default.createElement(
'span',
{ style: styles.fileName },
file.name + ', ' + file.type
),
_react2.default.createElement(
'span',
{ style: styles.fileSize },
sizeInMB + ' Mb'
),
_react2.default.createElement('span', { style: styles.removeButton, className: actionButtonClass, onClick: function onClick() {
return _this10.cancelFile(item.index);
} })
),
_react2.default.createElement(
'div',
null,
_react2.default.createElement(
'progress',
{ style: styles.progress, min: '0', max: '100', value: item.progress },
item.progress,
'%'
)
)
);
})
)
)
};
}();
if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v;
}
return _react2.default.createElement(_ReactCSSTransitionGroup2.default, { component: 'div', transitionName: transitionName, transitionEnterTimeout: 0, transitionLeaveTimeout: 0 });
}
}, {
key: 'renderButton',
value: function renderButton() {
var _this11 = this;
var styles = this.props.styles;
var displayButton = !this.props.auto;
if (displayButton) {
return _react2.default.createElement(
'button',
{ style: styles.uploadButtonStyle, onClick: function onClick() {
return _this11.upload();
} },
this.props.buttonLabel
);
}
return null;
}
}, {
key: 'renderInput',
value: function renderInput() {
var _this12 = this;
var maxFiles = this.props.maxFiles;
return _react2.default.createElement('input', { style: { display: 'none' }, multiple: maxFiles > 1, type: 'file', ref: 'fileInput', onChange: function onChange() {
return _this12.onFileSelect();
} });
}
}, {
key: 'render',
value: function render() {
var styles = this.props.styles;
return _react2.default.createElement(
'div',
{ style: styles.root },
this.renderDropTarget(),
this.renderFileSet(),
this.renderButton(),
this.renderInput()
);
}
}]);
return XHRUploader;
}(_react2.default.Component);
exports.default = XHRUploader;