var utils = require('./utils') var event = require('./event') var File = require('./file') var Chunk = require('./chunk') var version = '__VERSION__' var isServer = typeof window === 'undefined' // ie10+ var ie10plus = isServer ? false : window.navigator.msPointerEnabled var support = (function () { if (isServer) { return false } var sliceName = 'slice' var _support = utils.isDefined(window.File) && utils.isDefined(window.Blob) && utils.isDefined(window.FileList) var bproto = null if (_support) { bproto = window.Blob.prototype utils.each(['slice', 'webkitSlice', 'mozSlice'], function (n) { if (bproto[n]) { sliceName = n return false } }) _support = !!bproto[sliceName] } if (_support) Uploader.sliceName = sliceName bproto = null return _support })() var supportDirectory = (function () { if (isServer) { return false } var input = window.document.createElement('input') input.type = 'file' var sd = 'webkitdirectory' in input || 'directory' in input input = null return sd })() function Uploader (opts) { this.support = support /* istanbul ignore if */ if (!this.support) { return } this.supportDirectory = supportDirectory utils.defineNonEnumerable(this, 'filePaths', {}) this.opts = utils.extend({}, Uploader.defaults, opts || {}) this.preventEvent = utils.bind(this._preventEvent, this) File.call(this, this) } /** * Default read function using the webAPI * * @function webAPIFileRead(fileObj, fileType, startByte, endByte, chunk) * */ var webAPIFileRead = function (fileObj, fileType, startByte, endByte, chunk) { chunk.readFinished(fileObj.file[Uploader.sliceName](startByte, endByte, fileType)) } Uploader.version = version Uploader.defaults = { chunkSize: 1024 * 1024, forceChunkSize: false, simultaneousUploads: 3, singleFile: false, fileParameterName: 'file', progressCallbacksInterval: 500, speedSmoothingFactor: 0.1, query: {}, headers: {}, withCredentials: false, preprocess: null, method: 'multipart', testMethod: 'GET', uploadMethod: 'POST', prioritizeFirstAndLastChunk: false, allowDuplicateUploads: false, target: '/', testChunks: true, generateUniqueIdentifier: null, maxChunkRetries: 0, chunkRetryInterval: null, permanentErrors: [404, 415, 500, 501], successStatuses: [200, 201, 202], onDropStopPropagation: false, initFileFn: null, readFileFn: webAPIFileRead, checkChunkUploadedByResponse: null, initialPaused: false, processResponse: function (response, cb) { cb(null, response) }, processParams: function (params) { return params } } Uploader.utils = utils Uploader.event = event Uploader.File = File Uploader.Chunk = Chunk // inherit file Uploader.prototype = utils.extend({}, File.prototype) // inherit event utils.extend(Uploader.prototype, event) utils.extend(Uploader.prototype, { constructor: Uploader, _trigger: function (name) { var args = utils.toArray(arguments) var preventDefault = !this.trigger.apply(this, arguments) if (name !== 'catchAll') { args.unshift('catchAll') preventDefault = !this.trigger.apply(this, args) || preventDefault } return !preventDefault }, _triggerAsync: function () { var args = arguments utils.nextTick(function () { this._trigger.apply(this, args) }, this) }, addFiles: function (files, evt) { var _files = [] var oldFileListLen = this.fileList.length utils.each(files, function (file) { // Uploading empty file IE10/IE11 hangs indefinitely // Directories have size `0` and name `.` // Ignore already added files if opts.allowDuplicateUploads is set to false if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) { var uniqueIdentifier = this.generateUniqueIdentifier(file) if (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier)) { var _file = new File(this, file, this) _file.uniqueIdentifier = uniqueIdentifier if (this._trigger('fileAdded', _file, evt)) { _files.push(_file) } else { File.prototype.removeFile.call(this, _file) } } } }, this) // get new fileList var newFileList = this.fileList.slice(oldFileListLen) if (this._trigger('filesAdded', _files, newFileList, evt)) { utils.each(_files, function (file) { if (this.opts.singleFile && this.files.length > 0) { this.removeFile(this.files[0]) } this.files.push(file) }, this) this._trigger('filesSubmitted', _files, newFileList, evt) } else { utils.each(newFileList, function (file) { File.prototype.removeFile.call(this, file) }, this) } }, addFile: function (file, evt) { this.addFiles([file], evt) }, cancel: function () { for (var i = this.fileList.length - 1; i >= 0; i--) { this.fileList[i].cancel() } }, removeFile: function (file) { File.prototype.removeFile.call(this, file) this._trigger('fileRemoved', file) }, generateUniqueIdentifier: function (file) { var custom = this.opts.generateUniqueIdentifier if (utils.isFunction(custom)) { return custom(file) } /* istanbul ignore next */ // Some confusion in different versions of Firefox var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name /* istanbul ignore next */ return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '') }, getFromUniqueIdentifier: function (uniqueIdentifier) { var ret = false utils.each(this.files, function (file) { if (file.uniqueIdentifier === uniqueIdentifier) { ret = file return false } }) return ret }, uploadNextChunk: function (preventEvents) { var found = false var pendingStatus = Chunk.STATUS.PENDING var checkChunkUploaded = this.uploader.opts.checkChunkUploadedByResponse if (this.opts.prioritizeFirstAndLastChunk) { utils.each(this.files, function (file) { if (file.paused) { return } if (checkChunkUploaded && !file._firstResponse && file.isUploading()) { // waiting for current file's first chunk response return } if (file.chunks.length && file.chunks[0].status() === pendingStatus) { file.chunks[0].send() found = true return false } if (file.chunks.length > 1 && file.chunks[file.chunks.length - 1].status() === pendingStatus) { file.chunks[file.chunks.length - 1].send() found = true return false } }) if (found) { return found } } // Now, simply look for the next, best thing to upload utils.each(this.files, function (file) { if (!file.paused) { if (checkChunkUploaded && !file._firstResponse && file.isUploading()) { // waiting for current file's first chunk response return } utils.each(file.chunks, function (chunk) { if (chunk.status() === pendingStatus) { chunk.send() found = true return false } }) } if (found) { return false } }) if (found) { return true } // The are no more outstanding chunks to upload, check is everything is done var outstanding = false utils.each(this.files, function (file) { if (!file.isComplete()) { outstanding = true return false } }) // should check files now // if now files in list // should not trigger complete event if (!outstanding && !preventEvents && this.files.length) { // All chunks have been uploaded, complete this._triggerAsync('complete') } return outstanding }, upload: function (preventEvents) { // Make sure we don't start too many uploads at once var ret = this._shouldUploadNext() if (ret === false) { return } !preventEvents && this._trigger('uploadStart') var started = false for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) { started = this.uploadNextChunk(!preventEvents) || started if (!started && preventEvents) { // completed break } } if (!started && !preventEvents) { this._triggerAsync('complete') } }, /** * should upload next chunk * @function * @returns {Boolean|Number} */ _shouldUploadNext: function () { var num = 0 var should = true var simultaneousUploads = this.opts.simultaneousUploads var uploadingStatus = Chunk.STATUS.UPLOADING utils.each(this.files, function (file) { utils.each(file.chunks, function (chunk) { if (chunk.status() === uploadingStatus) { num++ if (num >= simultaneousUploads) { should = false return false } } }) return should }) // if should is true then return uploading chunks's length return should && num }, /** * Assign a browse action to one or more DOM nodes. * @function * @param {Element|Array.} domNodes * @param {boolean} isDirectory Pass in true to allow directories to * @param {boolean} singleFile prevent multi file upload * @param {Object} attributes set custom attributes: * http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes * eg: accept: 'image/*' * be selected (Chrome only). */ assignBrowse: function (domNodes, isDirectory, singleFile, attributes) { if (typeof domNodes.length === 'undefined') { domNodes = [domNodes] } utils.each(domNodes, function (domNode) { var input if (domNode.tagName === 'INPUT' && domNode.type === 'file') { input = domNode } else { input = document.createElement('input') input.setAttribute('type', 'file') // display:none - not working in opera 12 utils.extend(input.style, { visibility: 'hidden', position: 'absolute', width: '1px', height: '1px' }) // for opera 12 browser, input must be assigned to a document domNode.appendChild(input) // https://developer.mozilla.org/en/using_files_from_web_applications) // event listener is executed two times // first one - original mouse click event // second - input.click(), input is inside domNode domNode.addEventListener('click', function (e) { if (domNode.tagName.toLowerCase() === 'label') { return } input.click() }, false) } if (!this.opts.singleFile && !singleFile) { input.setAttribute('multiple', 'multiple') } if (isDirectory) { input.setAttribute('webkitdirectory', 'webkitdirectory') } attributes && utils.each(attributes, function (value, key) { input.setAttribute(key, value) }) // When new files are added, simply append them to the overall list var that = this input.addEventListener('change', function (e) { that._trigger(e.type, e) if (e.target.value) { that.addFiles(e.target.files, e) e.target.value = '' } }, false) }, this) }, onDrop: function (evt) { this._trigger(evt.type, evt) if (this.opts.onDropStopPropagation) { evt.stopPropagation() } evt.preventDefault() this._parseDataTransfer(evt.dataTransfer, evt) }, _parseDataTransfer: function (dataTransfer, evt) { if (dataTransfer.items && dataTransfer.items[0] && dataTransfer.items[0].webkitGetAsEntry) { this.webkitReadDataTransfer(dataTransfer, evt) } else { this.addFiles(dataTransfer.files, evt) } }, webkitReadDataTransfer: function (dataTransfer, evt) { var self = this var queue = dataTransfer.items.length var files = [] utils.each(dataTransfer.items, function (item) { var entry = item.webkitGetAsEntry() if (!entry) { decrement() return } if (entry.isFile) { // due to a bug in Chrome's File System API impl - #149735 fileReadSuccess(item.getAsFile(), entry.fullPath) } else { readDirectory(entry.createReader()) } }) function readDirectory (reader) { reader.readEntries(function (entries) { if (entries.length) { queue += entries.length utils.each(entries, function (entry) { if (entry.isFile) { var fullPath = entry.fullPath entry.file(function (file) { fileReadSuccess(file, fullPath) }, readError) } else if (entry.isDirectory) { readDirectory(entry.createReader()) } }) readDirectory(reader) } else { decrement() } }, readError) } function fileReadSuccess (file, fullPath) { // relative path should not start with "/" file.relativePath = fullPath.substring(1) files.push(file) decrement() } function readError (fileError) { throw fileError } function decrement () { if (--queue === 0) { self.addFiles(files, evt) } } }, _assignHelper: function (domNodes, handles, remove) { if (typeof domNodes.length === 'undefined') { domNodes = [domNodes] } var evtMethod = remove ? 'removeEventListener' : 'addEventListener' utils.each(domNodes, function (domNode) { utils.each(handles, function (handler, name) { domNode[evtMethod](name, handler, false) }, this) }, this) }, _preventEvent: function (e) { utils.preventEvent(e) this._trigger(e.type, e) }, /** * Assign one or more DOM nodes as a drop target. * @function * @param {Element|Array.} domNodes */ assignDrop: function (domNodes) { this._onDrop = utils.bind(this.onDrop, this) this._assignHelper(domNodes, { dragover: this.preventEvent, dragenter: this.preventEvent, dragleave: this.preventEvent, drop: this._onDrop }) }, /** * Un-assign drop event from DOM nodes * @function * @param domNodes */ unAssignDrop: function (domNodes) { this._assignHelper(domNodes, { dragover: this.preventEvent, dragenter: this.preventEvent, dragleave: this.preventEvent, drop: this._onDrop }, true) this._onDrop = null } }) module.exports = Uploader