452 lines
18 KiB
JavaScript
452 lines
18 KiB
JavaScript
/**
|
|
* File upload
|
|
* @augments EicComponent
|
|
* @category EICUI/Components
|
|
* @author Nike
|
|
* @version 1.0
|
|
*/
|
|
class FileUpload extends EicComponent {
|
|
|
|
/**
|
|
* Triggered just before upload of file(s) is launched
|
|
* Return False to prevent theupload to happen
|
|
*
|
|
* @event FileUpload#onBeforeUploadAllStart
|
|
* @type {event}
|
|
*/
|
|
onBeforeUploadAllStart() { return(true) }
|
|
|
|
/**
|
|
* Triggered when the upload of file(s) is launched
|
|
*
|
|
* @event FileUpload#onUploadAllStart
|
|
* @type {event}
|
|
*/
|
|
onUploadAllStart() { }
|
|
|
|
/**
|
|
* Triggered when the upload of file(s) is launched
|
|
*
|
|
* @event FileUpload#onUploadAllEnd
|
|
* @type {event}
|
|
*/
|
|
onUploadAllEnd() { }
|
|
|
|
/**
|
|
* Triggered just before upload of file(s) is launched
|
|
* Return False to prevent theupload to happen
|
|
*
|
|
* @event FileUpload#onBeforeUploadOneStart
|
|
* @type {event}
|
|
* @param {file} fileInfo The offening file object (https://developer.mozilla.org/en-US/docs/Web/API/File)
|
|
*/
|
|
onBeforeUploadOneStart(file) { return(true) }
|
|
|
|
/**
|
|
* Triggered when the upload of file(s) is launched
|
|
*
|
|
* @event FileUpload#onUploadOneStart
|
|
* @type {event}
|
|
* @param {file} fileInfo The offening file object (https://developer.mozilla.org/en-US/docs/Web/API/File)
|
|
*/
|
|
onUploadOneStart(file) { }
|
|
|
|
/**
|
|
* Triggered when the upload of file(s) is launched
|
|
*
|
|
* @event FileUpload#onUploadOneEnd
|
|
* @type {event}
|
|
* @param {file} fileInfo The offening file object (https://developer.mozilla.org/en-US/docs/Web/API/File)
|
|
*/
|
|
onUploadOneEnd(file) { }
|
|
|
|
/**
|
|
* Triggered on attempting to add a file with wrong extension/mime to the list
|
|
*
|
|
* @event FileUpload#onWrongFileType
|
|
* @type {event}
|
|
* @param {file} fileInfo The offening file object (https://developer.mozilla.org/en-US/docs/Web/API/File)
|
|
*/
|
|
onWrongFileType(file) { console.warn('Wrong file type !', file) }
|
|
|
|
/**
|
|
* Triggered when a file is added in the list
|
|
*
|
|
* @event FileUpload#onFileAdded
|
|
* @type {event}
|
|
* @param {integer} fileIdx The file index in this.filesList
|
|
* @param {file} fileInfo The added file object (https://developer.mozilla.org/en-US/docs/Web/API/File)
|
|
*/
|
|
onFileAdded(fileIdx, file) { }
|
|
|
|
/**
|
|
* Triggered when a file is removed from the list
|
|
*
|
|
* @event FileUpload#onFileRemoved
|
|
* @type {event}
|
|
* @param {integer} fileIdx The file index in this.filesList
|
|
* @param {file} fileInfo The removed file object (https://developer.mozilla.org/en-US/docs/Web/API/File)
|
|
*/
|
|
onFileRemoved(fileIdx, file) { }
|
|
|
|
|
|
|
|
/**
|
|
* @param {element|string} el A DOM element or the selector string for the element which will contain the component
|
|
* @param {object} [options] The grid options
|
|
* @param {string} [options.uploadUrl] Where to POST the files
|
|
* @param {string} [options.browseLabel] Text of the browser button
|
|
* @param {string} [options.submitLabel] Text of the upload button
|
|
* @param {string} [options.dndLabel] Text of the drop area
|
|
* @param {array} [options.allowedMimes] list of mim types accepted
|
|
* @param {array} [options.allowedExtensions] list of file extenstions accepted (dot excluded)
|
|
* @param {boolean} [options.allowDrop] Enables drag & drop
|
|
* @param {Integer} [options.maxFiles] Set to 1 to disable file list
|
|
* @param {Integer} [options.minFiles] Upload button will show only if files list greater or equal
|
|
* @param {object} [options.mimeTypes] Recognized mime types (unknown types show extension as name, and an attachment icon)
|
|
* @param {string} [options.mimetypes[mime].label] type name used in list
|
|
* @param {string} [options.mimetypes[mime].icon] icon used in list
|
|
* @param {string} [options.noUpload] Use when processiong files in browser : no upload to server, no upload/reset buttons
|
|
*/
|
|
constructor(el, options) {
|
|
super(el, options);
|
|
this.options = {
|
|
browseLabel: 'Choose file...',
|
|
submitLabel : 'upload',
|
|
resetLabel: 'clear list',
|
|
noUpload : false,
|
|
dndLabel: 'Drag and drop file here',
|
|
allowedMimes: ['application/pdf'],
|
|
allowedExtensions: ['pdf'],
|
|
allowDrop: true,
|
|
maxSize: 1024*1024*1024,
|
|
maxFiles: 1,
|
|
minFiles: 1,
|
|
uploadMethod: 'POST',
|
|
mimeTypes: {
|
|
'video/x-msvideo': {label: 'Video' , icon: 'youtube'},
|
|
'video/mp4': {label: 'Video' , icon: 'youtube'},
|
|
'video/mpeg': {label: 'Video' , icon: 'youtube'},
|
|
'video/ogg': {label: 'Video' , icon: 'youtube'},
|
|
|
|
'application/x-bzip': {label: 'Compressed file' , icon: 'zip'},
|
|
'application/x-bzip2': {label: 'Compressed file' , icon: 'zip'},
|
|
'application/gzip': {label: 'Compressed file' , icon: 'zip'},
|
|
'application/zip': {label: 'Compressed file' , icon: 'zip'},
|
|
'application/x-7z-compressed': {label: 'Compressed file' , icon: 'zip'},
|
|
|
|
'application/msword': {label: 'Text document' , icon: 'writing'},
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': {label: 'Text document' , icon: 'writing'},
|
|
'application/vnd.oasis.opendocument.text': {label: 'Text document' , icon: 'writing'},
|
|
|
|
'application/vnd.ms-excel': {label: 'Spreadsheet' , icon: 'xls'},
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {label: 'Spreadsheet' , icon: 'xls'},
|
|
'application/vnd.oasis.opendocument.spreadsheet': {label: 'Spreadsheet' , icon: 'xls'},
|
|
'text/csv': {label: 'Spreadsheet' , icon: 'xls'},
|
|
|
|
'image/gif': {label: 'Image' , icon: 'image'},
|
|
'image/jpeg': {label: 'Image' , icon: 'image'},
|
|
'image/png': {label: 'Image' , icon: 'image'},
|
|
'image/svg+xml': {label: 'Image' , icon: 'image'},
|
|
|
|
'application/pdf': {label: 'PDF document' , icon: 'pdf'},
|
|
}
|
|
};
|
|
|
|
this.setOptions(options);
|
|
this.el.setAttribute('eicfileupload', '');
|
|
this.fileItemTpl = `
|
|
<div class="file-preview-item">
|
|
<div class="file-icon">%{icon}</div>
|
|
<div class="file-name">%{name}</div>
|
|
<div class="file-size">%{size}</div>
|
|
<div class="file-type">%{type}</div>
|
|
<div class="file-actions">%{actions}</div>
|
|
</div>
|
|
`
|
|
this.browseBtn = new Button(null, {
|
|
icon: null,
|
|
severity: 'primary',
|
|
disabled: false,
|
|
badge: false,
|
|
rounded: false,
|
|
size: 'small',
|
|
hint: null,
|
|
label: this.options.browseLabel
|
|
})
|
|
this.browseBtn.click = (e) => {
|
|
e.stopPropagation(); e.preventDefault();
|
|
this.fileBtn.click()
|
|
}
|
|
|
|
if(!this.options.noUpload){
|
|
this.uploadBtn = new Button(null, {
|
|
icon: null,
|
|
severity: 'primary',
|
|
disabled: false,
|
|
badge: false,
|
|
rounded: false,
|
|
size: null,
|
|
hint: null,
|
|
label: this.options.submitLabel,
|
|
onclick: this.uploadAllFiles.bind(this)
|
|
})
|
|
this.uploadBtn.el.classList = 'uploadBtn custom-class'
|
|
|
|
this.resetBtn = new Button(null, {
|
|
icon: null,
|
|
severity: '',
|
|
disabled: false,
|
|
badge: false,
|
|
rounded: false,
|
|
size: null,
|
|
hint: null,
|
|
label: this.options.resetLabel
|
|
})
|
|
this.resetBtn.click = this.clearAllFiles.bind(this)
|
|
}
|
|
|
|
this.el.appendChild(ui.create(`
|
|
<div class="file-upload">
|
|
<div class="file-drop-area">
|
|
<span class="file-msg">${options.allowDrop ? this.options.dndLabel : ''}</span>
|
|
<input type="file" name="file" class="file-input" aria-label="Choose file ${options.allowDrop ? ' or '+ this.options.dndLabel : ''}">
|
|
</div>
|
|
<div class="file-preview-area"></div>
|
|
<div class="buttons-area cols-2"></div>
|
|
</div>
|
|
`))
|
|
|
|
this.fileBtn = this.el.querySelector('.file-input')
|
|
this.fileBtn.setAttribute('accept', [...this.options.allowedMimes, ...this.options.allowedExtensions.map(item => '.'+item)].join(', '))
|
|
if(this.options.maxFiles>1) this.fileBtn.setAttribute('multiple', '')
|
|
|
|
this.dropArea = this.el.querySelector('.file-drop-area')
|
|
this.dropAreaText = this.el.querySelector('.file-drop-area .file-msg')
|
|
this.dropArea.prepend(this.browseBtn.el)
|
|
this.previewArea = this.el.querySelector('.file-preview-area')
|
|
this.buttonsArea = this.el.querySelector('.buttons-area')
|
|
|
|
if(!this.options.noUpload){
|
|
this.buttonsArea.append(this.uploadBtn.el)
|
|
this.buttonsArea.append(this.resetBtn.el)
|
|
}
|
|
|
|
this.filesList = []
|
|
this.fileBtn.addEventListener("change", (e) => {
|
|
if((!e.target.files) || (e.target.files.length==0)) return
|
|
if((this.filesList.length + e.target.files.length) > this.options.maxFiles) return
|
|
this.addFiles(e.target.files)
|
|
})
|
|
if(options.allowDrop) {
|
|
this.initDnd()
|
|
this.enableDnd()
|
|
} else this.disableDnd()
|
|
|
|
this.refreshFilesList() //List still empty but that hides buttons depending on options
|
|
}
|
|
|
|
initDnd() {
|
|
window.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); })
|
|
window.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); })
|
|
this.dropArea.addEventListener('dragenter', ((e) => {
|
|
e.stopPropagation(); e.preventDefault();
|
|
if(this.dndDisabled) return
|
|
this.dropArea.classList.add('dragover')
|
|
}).bind(this));
|
|
this.dropArea.addEventListener('dragleave', ((e) =>{
|
|
e.stopPropagation(); e.preventDefault();
|
|
let elBelow = document.elementFromPoint(e.clientX, e.clientY)
|
|
if( (elBelow.classList.contains('file-drop-area'))
|
|
|| (elBelow.classList.contains('file-msg'))
|
|
|| (elBelow.classList.contains('file-input'))
|
|
) return //leaving towards the child, so still over it
|
|
if(this.dndDisabled) return
|
|
this.dropArea.classList.remove('dragover')
|
|
}).bind(this))
|
|
this.dropArea.addEventListener('drop', ((e) => {
|
|
e.stopPropagation(); e.preventDefault();
|
|
if(this.dndDisabled) return
|
|
this.dropArea.classList.remove('dragover')
|
|
if((!e.dataTransfer) || (!e.dataTransfer.files) || (e.dataTransfer.files.length==0)) return
|
|
if((this.filesList.length + e.dataTransfer.files.length) > this.options.maxFiles) return
|
|
this.addFiles(e.dataTransfer.files)
|
|
}).bind(this));
|
|
}
|
|
|
|
disableDnd() {
|
|
this.dndDisabled = true
|
|
this.dropAreaText.classList.add('disabled')
|
|
}
|
|
|
|
enableDnd() {
|
|
this.dndDisabled = false
|
|
this.dropAreaText.classList.remove('disabled')
|
|
}
|
|
|
|
addFiles(files) {
|
|
for(let file of files) {
|
|
if((file.type!='') && this.checkFileType(file) && (file.size <= this.options.maxSize)) {
|
|
this.filesList.push(file)
|
|
this.onFileAdded(this.filesList.length-1, file)
|
|
this.refreshFilesList()
|
|
} else {
|
|
this.onWrongFileType(file)
|
|
}
|
|
}
|
|
}
|
|
|
|
removeFile(e) {
|
|
e.stopPropagation(); e.preventDefault();
|
|
let el = (e.target.tagName.toLocaleLowerCase()=='button') ? e.target : e.target.parentElement
|
|
let fileIdx = el.dataset['id']
|
|
let [file, ] = this.filesList.splice(fileIdx,1)
|
|
this.refreshFilesList()
|
|
this.onFileRemoved(fileIdx, file)
|
|
}
|
|
|
|
checkFileType(file) {
|
|
let ext = file.name.substring(file.name.lastIndexOf('.')+1)
|
|
return((this.options.allowedMimes.indexOf(file.type)>-1) && (this.options.allowedExtensions.indexOf(ext)>-1))
|
|
}
|
|
|
|
refreshFilesList() {
|
|
let html = ''; let itemLine, file, ext, icon, type;
|
|
for(let fileIdx in this.filesList) {
|
|
file = this.filesList[fileIdx]
|
|
ext = file.name.substring(file.name.lastIndexOf('.')+1)
|
|
if(this.options.mimeTypes.hasOwnProperty(file.type)) {
|
|
icon = this.options.mimeTypes[file.type].icon
|
|
type = this.options.mimeTypes[file.type].label
|
|
} else {
|
|
icon = 'attachment'
|
|
type = '".'+ext+'"'
|
|
}
|
|
icon = (this.options.mimeTypes.hasOwnProperty(file.type)) ? this.options.mimeTypes[file.type].icon : 'attachment'
|
|
itemLine = this.fileItemTpl
|
|
itemLine = itemLine.replace('%{icon}', `<i class="icon-${icon}"></i>`)
|
|
itemLine = itemLine.replace('%{size}', this.formatSize(file.size))
|
|
itemLine = itemLine.replace('%{type}', type)
|
|
itemLine = itemLine.replace('%{actions}', `
|
|
<button eicbutton class="file-remove" data-id="${fileIdx}" basic danger xxsmall title="remove" aria-enabled="true" aria-label="remove">
|
|
remove
|
|
</button>
|
|
`)
|
|
itemLine = itemLine.replace('%{name}', file.name) // Do this one last in case some %{xxx} in the name
|
|
html += itemLine
|
|
}
|
|
this.previewArea.innerHTML = html // Caution to myself: Re-creating all avoids multi-click-events
|
|
this.previewArea.querySelectorAll('.file-remove').forEach((el) => {
|
|
el.addEventListener('click', this.removeFile.bind(this))
|
|
})
|
|
|
|
if(!this.options.noUpload){
|
|
if(this.filesList.length >= this.options.minFiles) this.uploadBtn.disabled = false
|
|
else this.uploadBtn.disabled = true
|
|
}
|
|
|
|
if(this.filesList.length < this.options.maxFiles) {
|
|
this.browseBtn.disabled = false
|
|
this.enableDnd()
|
|
} else {
|
|
this.browseBtn.disabled = true
|
|
this.disableDnd()
|
|
}
|
|
|
|
if(!this.options.noUpload){
|
|
if(this.filesList.length > 0) this.resetBtn.disabled = false
|
|
else this.resetBtn.disabled = true
|
|
}
|
|
|
|
}
|
|
|
|
formatSize(size) {
|
|
if (!+size) return '0 Bytes'
|
|
const k = 1024
|
|
const units = ['Bytes', 'KB', 'MB', 'GB'] // Over GB, really ?
|
|
const i = Math.floor(Math.log(size) / Math.log(k))
|
|
return `${parseFloat((size / Math.pow(k, i)).toFixed(2))} ${units[i]}`
|
|
}
|
|
|
|
clearAllFiles(e) {
|
|
e.stopPropagation(); e.preventDefault();
|
|
this.filesList = []
|
|
this.refreshFilesList()
|
|
if(this.options.allowDrop) this.enableDnd()
|
|
}
|
|
|
|
uploadAllFiles(e) {
|
|
e.stopPropagation(); e.preventDefault();
|
|
if(!this.onBeforeUploadAllStart()) return
|
|
this.browseBtn.disabled = true
|
|
this.disableDnd()
|
|
this.uploadBtn.loading = true
|
|
this.resetBtn.disabled = true
|
|
for(let fileIdx in this.filesList) {
|
|
this.filesList[fileIdx].completed = false
|
|
this.uploadFile(fileIdx)
|
|
}
|
|
this.onUploadAllStart()
|
|
}
|
|
|
|
async uploadFile(fileIdx) {
|
|
let file = this.filesList[fileIdx]
|
|
let actionEl = this.previewArea.querySelector('.file-remove[data-id="'+fileIdx+'"]').parentElement
|
|
|
|
// Check file type --> if img add extension
|
|
let ext = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase();
|
|
if (['jpeg', 'png', 'gif', 'jpg', 'svg'].includes(ext)) {
|
|
this.options.uploadUrl += '.' + ext; // Add extension
|
|
}
|
|
|
|
let ok = await this.onBeforeUploadOneStart(file)
|
|
if((!ok) || (!this.options.uploadUrl)) {
|
|
this.filesList[fileIdx].completed = true
|
|
actionEl.innerHTML = '<span class="err">Failed !</span>'
|
|
this.uploadOneEnd(fileIdx)
|
|
return
|
|
}
|
|
actionEl.innerHTML = '(0 %)'
|
|
let xhrRequest = new XMLHttpRequest();
|
|
xhrRequest.open(this.options.uploadMethod, this.options.uploadUrl);
|
|
// Caution: This fucking AWS shit uses Content-Type as mime-type for the file, contrary to the rest of the web.
|
|
// Yeah, I know multi-files...multi-part etc...but AWS signed URL system is one file at a time anyway.
|
|
xhrRequest.setRequestHeader('Content-Type', this.filesList[fileIdx].type);
|
|
xhrRequest.upload.addEventListener("progress", this.uploadProgress.bind(this, fileIdx, actionEl));
|
|
xhrRequest.upload.addEventListener("error", ((err) => {
|
|
this.filesList[fileIdx].completed = true
|
|
actionEl.innerHTML = '<span class="err">Failed !</span>'
|
|
this.uploadOneEnd(fileIdx)
|
|
}).bind(this))
|
|
xhrRequest.upload.addEventListener("timeout", ((err) => {
|
|
this.filesList[fileIdx].completed = true
|
|
actionEl.innerHTML = '<span class="err">Timed-out !</span>'
|
|
this.uploadOneEnd(fileIdx)
|
|
}).bind(this))
|
|
xhrRequest.send(file);
|
|
this.onUploadOneStart()
|
|
}
|
|
|
|
uploadProgress(fileIdx, actionEl, e) {
|
|
if(!e.lengthComputable) return
|
|
let percentDone = Math.floor((e.loaded / e.total) * 100);
|
|
actionEl.innerHTML = `(${percentDone} %)`
|
|
if(e.loaded == e.total) {
|
|
this.filesList[fileIdx].completed = true
|
|
this.uploadOneEnd(fileIdx)
|
|
}
|
|
}
|
|
|
|
uploadOneEnd(fileIdx) {
|
|
this.onUploadOneEnd(this.filesList[fileIdx])
|
|
let allFinished = this.filesList.reduce((acc, x)=> (x.completed && acc), true)
|
|
if(allFinished) {
|
|
this.uploadBtn.loading = false
|
|
this.uploadBtn.disabled = true
|
|
this.resetBtn.disabled = false
|
|
this.onUploadAllEnd()
|
|
}
|
|
}
|
|
|
|
}
|