@braid/vue-formulate
Version:
The easiest way to build forms in Vue.
296 lines (279 loc) • 8.88 kB
JavaScript
import { setId, token } from './libs/utils'
/**
* The file upload class holds and represents a file’s upload state durring
* the upload flow.
*/
class FileUpload {
/**
* Create a file upload object.
* @param {FileList} fileList
* @param {object} context
*/
constructor (input, context, globalOptions = {}) {
this.input = input
this.fileList = input.files
this.files = []
this.options = {
...{ mimes: {} },
...globalOptions
}
this.results = false
this.context = context
this.dataTransferCheck()
if (context && context.uploadUrl) {
this.options.uploadUrl = context.uploadUrl
}
this.uploadPromise = null
if (Array.isArray(this.fileList)) {
this.rehydrateFileList(this.fileList)
} else {
this.addFileList(this.fileList)
}
}
/**
* Given a pre-existing array of files, create a faux FileList.
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
* @param {string} pathKey the object-key to access the url (defaults to "url")
*/
rehydrateFileList (items) {
const fauxFileList = items.reduce((fileList, item) => {
const key = this.options ? this.options.fileUrlKey : 'url'
const url = item[key]
const ext = (url && url.lastIndexOf('.') !== -1) ? url.substr(url.lastIndexOf('.') + 1) : false
const mime = this.options.mimes[ext] || false
fileList.push(Object.assign({}, item, url ? {
name: item.name || url.substr((url.lastIndexOf('/') + 1) || 0),
type: item.type ? item.type : mime,
previewData: url
} : {}))
return fileList
}, [])
this.addFileList(fauxFileList)
this.results = this.mapUUID(items)
}
/**
* Produce an array of files and alert the callback.
* @param {FileList}
*/
addFileList (fileList) {
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i]
const uuid = token()
const removeFile = function () {
this.removeFile(uuid)
}
this.files.push({
progress: false,
error: false,
complete: false,
justFinished: false,
name: file.name || 'file-upload',
file,
uuid,
path: false,
removeFile: removeFile.bind(this),
previewData: file.previewData || false
})
}
}
/**
* Check if the file has an.
*/
hasUploader () {
return !!this.context.uploader
}
/**
* Check if the given uploader is axios instance. This isn't a great way of
* testing if it is or not, but AFIK there isn't a better way right now:
*
* https://github.com/axios/axios/issues/737
*/
uploaderIsAxios () {
if (
this.hasUploader() &&
typeof this.context.uploader.request === 'function' &&
typeof this.context.uploader.get === 'function' &&
typeof this.context.uploader.delete === 'function' &&
typeof this.context.uploader.post === 'function'
) {
return true
}
return false
}
/**
* Get a new uploader function.
*/
getUploader (...args) {
if (this.uploaderIsAxios()) {
const formData = new FormData()
formData.append(this.context.name || 'file', args[0])
if (this.context.uploadUrl === false) {
throw new Error('No uploadURL specified: https://vueformulate.com/guide/inputs/file/#props')
}
return this.context.uploader.post(this.context.uploadUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: progressEvent => {
// args[1] here is the upload progress handler function
args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total))
}
})
.then(res => res.data)
.catch(err => args[2](err))
}
return this.context.uploader(...args)
}
/**
* Perform the file upload.
*/
upload () {
// If someone calls upload() when an upload is already in process or there
// already was an upload completed, chain the upload request to the
// existing one. Already uploaded files wont re-upload and it ensures any
// files that were added after the initial list are completed too.
this.uploadPromise = this.uploadPromise
? this.uploadPromise.then(() => this.__performUpload())
: this.__performUpload()
return this.uploadPromise
}
/**
* Perform the actual upload event. Intended to be a private method that is
* only called through the upload() function as chaining utility.
*/
__performUpload () {
return new Promise((resolve, reject) => {
if (!this.hasUploader()) {
return reject(new Error('No uploader has been defined'))
}
Promise.all(this.files.map(file => {
file.error = false
file.complete = !!file.path
return file.path ? Promise.resolve(file.path) : this.getUploader(
file.file,
(progress) => {
file.progress = progress
this.context.rootEmit('file-upload-progress', progress)
if (progress >= 100) {
if (!file.complete) {
file.justFinished = true
setTimeout(() => { file.justFinished = false }, this.options.uploadJustCompleteDuration)
}
file.complete = true
this.context.rootEmit('file-upload-complete', file)
}
},
(error) => {
file.progress = 0
file.error = error
file.complete = true
this.context.rootEmit('file-upload-error', error)
reject(error)
},
this.options
)
}))
.then(results => {
this.results = this.mapUUID(results)
resolve(results)
})
.catch(err => {
throw new Error(err)
})
})
}
/**
* Remove a file from the uploader (and the file list)
* @param {string} uuid
*/
removeFile (uuid) {
const originalLength = this.files.length
this.files = this.files.filter(file => file && file.uuid !== uuid)
if (Array.isArray(this.results)) {
this.results = this.results.filter(file => file && file.__id !== uuid)
}
this.context.performValidation()
if (window && this.fileList instanceof FileList && this.supportsDataTransfers) {
const transfer = new DataTransfer()
this.files.forEach(file => transfer.items.add(file.file))
this.fileList = transfer.files
this.input.files = this.fileList
} else {
this.fileList = this.fileList.filter(file => file && file.__id !== uuid)
}
if (originalLength > this.files.length) {
this.context.rootEmit('file-removed', this.files)
}
}
/**
* Given another input element, add the files from that FileList to the
* input being represented by this FileUpload.
*
* @param {HTMLElement} input
*/
mergeFileList (input) {
this.addFileList(input.files)
// Create a new mutable FileList
if (this.supportsDataTransfers) {
const transfer = new DataTransfer()
this.files.forEach(file => {
if (file.file instanceof File) {
transfer.items.add(file.file)
}
})
this.fileList = transfer.files
this.input.files = this.fileList
// Reset the merged FileList to empty
input.files = (new DataTransfer()).files
}
this.context.performValidation()
this.loadPreviews()
if (this.context.uploadBehavior !== 'delayed') {
this.upload()
}
}
/**
* load image previews for all uploads.
*/
loadPreviews () {
this.files.map(file => {
if (!file.previewData && window && window.FileReader && /^image\//.test(file.file.type)) {
const reader = new FileReader()
reader.onload = e => Object.assign(file, { previewData: e.target.result })
reader.readAsDataURL(file.file)
}
})
}
/**
* Check if the current browser supports the DataTransfer constructor.
*/
dataTransferCheck () {
try {
new DataTransfer() // eslint-disable-line
this.supportsDataTransfers = true
} catch (err) {
this.supportsDataTransfers = false
}
}
/**
* Get the files.
*/
getFiles () {
return this.files
}
/**
* Run setId on each item of a pre-existing array of items.
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
*/
mapUUID (items) {
return items.map((result, index) => {
this.files[index].path = result !== undefined ? result : false
return result && setId(result, this.files[index].uuid)
})
}
toString () {
const descriptor = this.files.length ? this.files.length + ' files' : 'empty'
return this.results ? JSON.stringify(this.results, null, ' ') : `FileUpload(${descriptor})`
}
}
export default FileUpload