/* * HSFileUpload * @version: 3.2.3 * @author: Preline Labs Ltd. * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html) * Copyright 2024 Preline Labs Ltd. */ import { DropzoneFile } from 'dropzone' import { htmlToElement, classToClassList } from '../../utils' import { IFileUploadOptions, IFileUpload } from './interfaces' import HSBasePlugin from '../base-plugin' import { ICollectionItem } from '../../interfaces' declare var _: any declare var Dropzone: any if (typeof Dropzone !== 'undefined') Dropzone.autoDiscover = false class HSFileUpload extends HSBasePlugin implements IFileUpload { private concatOptions: IFileUploadOptions private previewTemplate: string private extensions: any = {} private singleton: boolean public dropzone: Dropzone | null private onReloadButtonClickListener: | { el: Element fn: (evt: MouseEvent) => void }[] | null private onTempFileInputChangeListener: | { el: HTMLElement fn: (event: Event) => void }[] | null constructor(el: HTMLElement, options?: IFileUploadOptions, events?: {}) { super(el, options, events) this.el = typeof el === 'string' ? document.querySelector(el) : el const data = this.el.getAttribute('data-file-upload') const dataOptions: IFileUploadOptions = data ? JSON.parse(data) : {} this.previewTemplate = this.el.querySelector('[data-file-upload-preview]')?.innerHTML || `

.

0%
` this.extensions = _.merge( { default: { icon: '', class: 'shrink-0 size-5' }, xls: { icon: '', class: 'shrink-0 size-5' }, doc: { icon: '', class: 'shrink-0 size-5' }, zip: { icon: '', class: 'shrink-0 size-5' } }, dataOptions.extensions ) this.singleton = dataOptions.singleton this.concatOptions = { clickable: this.el.querySelector('[data-file-upload-trigger]') as HTMLElement, previewsContainer: this.el.querySelector('[data-file-upload-previews]') as HTMLElement, addRemoveLinks: false, previewTemplate: this.previewTemplate, autoHideTrigger: false, ...dataOptions, ...options } this.onReloadButtonClickListener = [] this.onTempFileInputChangeListener = [] this.init() } private tempFileInputChange(event: Event, file: DropzoneFile) { const input = event.target as HTMLInputElement const newFile = input.files?.[0] if (newFile) { const dzNewFile = newFile as Dropzone.DropzoneFile dzNewFile.status = Dropzone.ADDED dzNewFile.accepted = true dzNewFile.previewElement = file.previewElement dzNewFile.previewTemplate = file.previewTemplate dzNewFile.previewsContainer = file.previewsContainer this.dropzone.removeFile(file) this.dropzone.addFile(dzNewFile) } } private reloadButtonClick(evt: MouseEvent, file: DropzoneFile) { evt.preventDefault() evt.stopPropagation() const tempFileInput = document.createElement('input') tempFileInput.type = 'file' this.onTempFileInputChangeListener.push({ el: tempFileInput, fn: (event: Event) => this.tempFileInputChange(event, file) }) tempFileInput.click() tempFileInput.addEventListener('change', this.onTempFileInputChangeListener.find(el => el.el === tempFileInput).fn) } private init() { this.createCollection(window.$hsFileUploadCollection, this) this.initDropzone() } private initDropzone() { const clear = this.el.querySelector('[data-file-upload-clear]') as HTMLButtonElement const pseudoTriggers = Array.from( this.el.querySelectorAll('[data-file-upload-pseudo-trigger]') ) as HTMLButtonElement[] this.dropzone = new Dropzone(this.el, this.concatOptions) this.dropzone.on('addedfile', (file: DropzoneFile) => this.onAddFile(file)) this.dropzone.on('removedfile', () => this.onRemoveFile()) this.dropzone.on('uploadprogress', (file: DropzoneFile, progress: number) => this.onUploadProgress(file, progress)) this.dropzone.on('complete', (file: DropzoneFile) => this.onComplete(file)) if (clear) clear.onclick = () => { if (this.dropzone.files.length) this.dropzone.removeAllFiles(true) } if (pseudoTriggers.length) pseudoTriggers.forEach(el => { el.onclick = () => { if (this.concatOptions?.clickable) (this.concatOptions?.clickable as HTMLButtonElement).click() } }) } // Public methods public destroy() { this.onTempFileInputChangeListener.forEach(el => { el.el.removeEventListener('change', el.fn) }) this.onTempFileInputChangeListener = null this.onReloadButtonClickListener.forEach(el => { el.el.removeEventListener('click', el.fn) }) this.onReloadButtonClickListener = null this.dropzone.destroy() window.$hsFileUploadCollection = window.$hsFileUploadCollection.filter(({ element }) => element.el !== this.el) } private onAddFile(file: DropzoneFile) { const { previewElement } = file const reloadButton = file.previewElement.querySelector('[data-file-upload-reload]') if (!previewElement) return false if (this.singleton && this.dropzone.files.length > 1) this.dropzone.removeFile(this.dropzone.files[0]) if (reloadButton) { this.onReloadButtonClickListener.push({ el: reloadButton, fn: evt => this.reloadButtonClick(evt, file) }) reloadButton.addEventListener('click', this.onReloadButtonClickListener.find(el => el.el === reloadButton).fn) } this.previewAccepted(file) } private previewAccepted(file: DropzoneFile) { const { previewElement } = file const fileInfo = this.splitFileName(file.name) const fileName = previewElement.querySelector('[data-file-upload-file-name]') const fileExt = previewElement.querySelector('[data-file-upload-file-ext]') const fileSize = previewElement.querySelector('[data-file-upload-file-size]') const fileIcon = previewElement.querySelector('[data-file-upload-file-icon]') as HTMLElement const trigger = this.el.querySelector('[data-file-upload-trigger]') as HTMLElement const preview = previewElement.querySelector('[data-dz-thumbnail]') as HTMLElement const remove = previewElement.querySelector('[data-file-upload-remove]') as HTMLButtonElement if (fileName) fileName.textContent = fileInfo.name if (fileExt) fileExt.textContent = fileInfo.extension if (fileSize) fileSize.textContent = this.formatFileSize(file.size) if (preview) { if (file.type.includes('image/')) preview.classList.remove('hidden') else this.setIcon(fileInfo.extension, fileIcon) } if (this.dropzone.files.length > 0 && this.concatOptions.autoHideTrigger) trigger.style.display = 'none' if (remove) remove.onclick = () => this.dropzone.removeFile(file) } private onRemoveFile() { const trigger = this.el.querySelector('[data-file-upload-trigger]') as HTMLElement if (this.dropzone.files.length === 0 && this.concatOptions.autoHideTrigger) trigger.style.display = '' } private onUploadProgress(file: DropzoneFile, progress: number) { const { previewElement } = file if (!previewElement) return false const progressBar = previewElement.querySelector('[data-file-upload-progress-bar]') const progressBarPane = previewElement.querySelector('[data-file-upload-progress-bar-pane]') as HTMLElement const progressBarValue = previewElement.querySelector('[data-file-upload-progress-bar-value]') as HTMLElement const currentProgress = Math.floor(progress) if (progressBar) progressBar.setAttribute('aria-valuenow', `${currentProgress}`) if (progressBarPane) progressBarPane.style.width = `${currentProgress}%` if (progressBarValue) progressBarValue.innerText = `${currentProgress}` } private onComplete(file: DropzoneFile) { const { previewElement } = file if (!previewElement) return false previewElement.classList.add('complete') } private setIcon(ext: string, file: HTMLElement) { const icon = this.createIcon(ext) file.append(icon) } private createIcon(ext: string) { const icon = this.extensions[ext]?.icon ? htmlToElement(this.extensions[ext].icon) : htmlToElement(this.extensions.default.icon) classToClassList(this.extensions[ext]?.class ? this.extensions[ext].class : this.extensions.default.class, icon) return icon } private formatFileSize(size: number) { if (size < 1024) { return size.toFixed(2) + ' B' } else if (size < 1024 * 1024) { return (size / 1024).toFixed(2) + ' KB' } else if (size < 1024 * 1024 * 1024) { return (size / (1024 * 1024)).toFixed(2) + ' MB' } else if (size < 1024 * 1024 * 1024 * 1024) { return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB' } else { return (size / (1024 * 1024 * 1024 * 1024)).toFixed(2) + ' TB' } } private splitFileName(file: string): { name: string extension: string } { let dotIndex = file.lastIndexOf('.') if (dotIndex == -1) return { name: file, extension: '' } return { name: file.substring(0, dotIndex), extension: file.substring(dotIndex + 1) } } // Static methods static getInstance(target: HTMLElement | string, isInstance?: boolean) { const elInCollection = window.$hsFileUploadCollection.find( el => el.element.el === (typeof target === 'string' ? document.querySelector(target) : target) ) return elInCollection ? (isInstance ? elInCollection : elInCollection.element.el) : null } static autoInit() { if (!window.$hsFileUploadCollection) window.$hsFileUploadCollection = [] if (window.$hsFileUploadCollection) window.$hsFileUploadCollection = window.$hsFileUploadCollection.filter(({ element }) => document.contains(element.el) ) document.querySelectorAll('[data-file-upload]:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => { if (!window.$hsFileUploadCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) new HSFileUpload(el) }) } } declare global { interface Window { HSFileUpload: Function $hsFileUploadCollection: ICollectionItem[] } } window.addEventListener('load', () => { if (document.querySelectorAll('[data-file-upload]:not(.--prevent-on-load-init)').length) { if (typeof _ === 'undefined') console.error('HSFileUpload: Lodash is not available, please add it to the page.') if (typeof Dropzone === 'undefined') console.error('HSFileUpload: Dropzone is not available, please add it to the page.') } if (typeof _ !== 'undefined' && typeof Dropzone !== 'undefined') { HSFileUpload.autoInit() } // Uncomment for debug // console.log('File upload collection:', window.$hsFileUploadCollection); }) if (typeof window !== 'undefined') { window.HSFileUpload = HSFileUpload } export default HSFileUpload