/* * HSFileUpload * @version: 4.1.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 '../file-upload/interfaces'; import HSBasePlugin from '../base-plugin'; 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-hs-file-upload'); const dataOptions: IFileUploadOptions = data ? JSON.parse(data) : {}; this.previewTemplate = this.el.querySelector('[data-hs-file-upload-preview]')?.innerHTML || `

.

0%
`; this.extensions = _.merge( { default: { icon: '', class: 'size-5', }, xls: { icon: '', class: 'size-5', }, doc: { icon: '', class: 'size-5', }, zip: { icon: '', class: 'size-5', }, }, dataOptions.extensions, ); this.singleton = dataOptions.singleton; this.concatOptions = { clickable: this.el.querySelector( '[data-hs-file-upload-trigger]', ) as HTMLElement, previewsContainer: this.el.querySelector( '[data-hs-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-hs-file-upload-clear]', ) as HTMLButtonElement; const pseudoTriggers = Array.from( this.el.querySelectorAll('[data-hs-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-hs-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-hs-file-upload-file-name]', ); const fileExt = previewElement.querySelector( '[data-hs-file-upload-file-ext]', ); const fileSize = previewElement.querySelector( '[data-hs-file-upload-file-size]', ); const fileIcon = previewElement.querySelector( '[data-hs-file-upload-file-icon]', ) as HTMLElement; const trigger = this.el.querySelector( '[data-hs-file-upload-trigger]', ) as HTMLElement; const preview = previewElement.querySelector( '[data-dz-thumbnail]', ) as HTMLElement; const remove = previewElement.querySelector( '[data-hs-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-hs-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-hs-file-upload-progress-bar]', ); const progressBarPane = previewElement.querySelector( '[data-hs-file-upload-progress-bar-pane]', ) as HTMLElement; const progressBarValue = previewElement.querySelector( '[data-hs-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-hs-file-upload]:not(.--prevent-on-load-init)') .forEach((el: HTMLElement) => { if ( !window.$hsFileUploadCollection.find( (elC) => (elC?.element?.el as HTMLElement) === el, ) ) new HSFileUpload(el); }); } } export default HSFileUpload;