Files
P42_UI/app/thirdparty/eicui/plugins/FileUpload/FileUpload.js
T
2025-08-27 07:03:09 +00:00

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()
}
}
}