| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- 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.<Element>} 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.<Element>} 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
|